#!/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]
PROMOTION = PROJECT / "story/promotion-report.json"
LEXICON_PATH = PROJECT / "script/pronunciation-lexicon.json"
LEXICON = []
SOURCES = [
    {
        "kind": "narration",
        "source": PROJECT / "story/spoken-narration.txt",
        "output": PROJECT / "story/tts-pronunciation.txt",
        "receipt": PROJECT / "log/tts-pronunciation.json",
        "required_only_if_present": True,
    },
    {
        "kind": "intro",
        "source": PROJECT / "audio/intro-voice-source.txt",
        "output": PROJECT / "audio/intro-voice-pronunciation.txt",
        "receipt": PROJECT / "log/intro-tts-pronunciation.json",
        "required_only_if_present": False,
    },
]


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


def sha_file(path):
    return sha_bytes(path.read_bytes())


def pattern(token):
    return re.compile(r"(?<![0-9A-Za-zÀ-ỹĐđ])" + re.escape(token) + r"(?![0-9A-Za-zÀ-ỹĐđ])", re.IGNORECASE)


def project(source_text):
    ordered = sorted(LEXICON, key=lambda row: len(row["source"]), reverse=True)
    by_source = {row["source"].casefold(): row for row in ordered}
    alternatives = "|".join(re.escape(row["source"]) for row in ordered)
    rx = re.compile(
        r"(?<![0-9A-Za-zÀ-ỹĐđ])(?:" + alternatives + r")(?![0-9A-Za-zÀ-ỹĐđ])",
        re.IGNORECASE,
    )
    parts = []
    replacements = []
    source_cursor = 0
    output_cursor = 0
    for match in rx.finditer(source_text):
        unchanged = source_text[source_cursor:match.start()]
        parts.append(unchanged)
        output_cursor += len(unchanged)
        original = match.group(0)
        item = by_source[original.casefold()]
        spoken = item["spoken"]
        parts.append(spoken)
        replacements.append({
            "source": original,
            "lexicon_source": item["source"],
            "spoken": spoken,
            "source_start": match.start(),
            "source_end": match.end(),
            "output_start": output_cursor,
            "output_end": output_cursor + len(spoken),
        })
        output_cursor += len(spoken)
        source_cursor = match.end()
    parts.append(source_text[source_cursor:])
    return "".join(parts), replacements


def reverse_projection(output_text, replacements):
    text = output_text
    for row in reversed(replacements):
        start = row["output_start"]
        end = row["output_end"]
        if text[start:end] != row["spoken"]:
            raise RuntimeError("reverse verification offset mismatch")
        text = text[:start] + row["source"] + text[end:]
    return text


def main():
    global LEXICON
    if not PROMOTION.is_file():
        raise RuntimeError("promotion report missing")
    if not LEXICON_PATH.is_file():
        raise RuntimeError("project pronunciation lexicon missing")
    lexicon_doc = json.loads(LEXICON_PATH.read_text(encoding="utf-8"))
    if lexicon_doc.get("verified") is not True or lexicon_doc.get("voice") != "ngoc-huyen-vbee":
        raise RuntimeError("project pronunciation lexicon invalid")
    LEXICON = [
        {
            "source": row["source"],
            "spoken": row["spoken"],
            "required": row.get("required_for_intro") is True,
        }
        for row in lexicon_doc.get("entries", [])
    ]
    if not LEXICON:
        raise RuntimeError("project pronunciation lexicon empty")
    promotion = json.loads(PROMOTION.read_text(encoding="utf-8"))
    canon = promotion.get("spoken_sha256")
    if promotion.get("verified") is not True or not canon:
        raise RuntimeError("promotion authority invalid")
    results = []
    for spec in SOURCES:
        source = spec["source"]
        output = spec["output"]
        receipt_path = spec["receipt"]
        if not source.is_file():
            raise RuntimeError(f"missing TTS source: {source}")
        source_text = source.read_text(encoding="utf-8")
        if not source_text.strip():
            raise RuntimeError(f"empty TTS source: {source}")
        projected, replacements = project(source_text)
        reconstructed = reverse_projection(projected, replacements)
        reverse_verified = reconstructed.encode("utf-8") == source_text.encode("utf-8")
        counts = []
        for item in LEXICON:
            source_count = len(pattern(item["source"]).findall(source_text))
            replacement_count = sum(1 for row in replacements if row["lexicon_source"].casefold() == item["source"].casefold())
            required = item["required"] and (not spec["required_only_if_present"] or source_count > 0)
            counts.append({**item, "required": required, "source_count": source_count, "replacement_count": replacement_count})
        unresolved = [
            row["source"] for row in counts
            if row["source_count"] != row["replacement_count"]
            or (row["required"] and row["source_count"] == 0)
        ]
        verified = reverse_verified and not unresolved
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(projected, encoding="utf-8")
        if sha_file(output) != sha_bytes(projected.encode("utf-8")):
            raise RuntimeError("pronunciation output read-back mismatch")
        receipt = {
            "version": 1,
            "verified": verified,
            "kind": spec["kind"],
            "source_canon_sha256": canon,
            "source_path": str(source.relative_to(PROJECT)),
            "source_sha256": sha_file(source),
            "projection_path": str(output.relative_to(PROJECT)),
            "projection_sha256": sha_file(output),
            "lexicon_path": str(LEXICON_PATH.relative_to(PROJECT)),
            "lexicon_sha256": sha_file(LEXICON_PATH),
            "lexicon": counts,
            "replacement_count": len(replacements),
            "replacements": replacements,
            "reverse_verified": reverse_verified,
            "non_lexicon_changes": 0,
            "semantic_content_changed": False,
            "unresolved_required": unresolved,
            "checked_at": datetime.now(timezone.utc).isoformat(),
        }
        receipt_path.parent.mkdir(parents=True, exist_ok=True)
        receipt_path.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
        if not verified:
            raise RuntimeError(f"pronunciation gate failed for {spec['kind']}: {unresolved}")
        results.append({"kind": spec["kind"], "replacements": len(replacements), "projection_sha256": receipt["projection_sha256"]})
    print(json.dumps({"verified": True, "sources": results}, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:
        print(f"Pronunciation gate blocked: {exc}", file=sys.stderr)
        sys.exit(1)
