#!/usr/bin/env python3
import datetime
import hashlib
import json
import os
import re
import unicodedata
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
TRANSFORM_VERSION = "gma-pronunciation-v1"
INTRO_SOURCE = "Các bạn đang nghe truyện được phát từ Gác Mái Audio, chúc các bạn có những giây phút nghe truyện vui vẻ. Hãy ủng hộ chúng tôi bằng cách like video và đăng ký kênh."
LEXICON_ENTRIES = [
    {"source": "05 giờ 12 phút", "spoken": "năm giờ mười hai phút", "scope": ["narration"], "reason": "expand exact clock expression for natural Vietnamese TTS"},
    {"source": "05 giờ 09", "spoken": "năm giờ không chín phút", "scope": ["narration"], "reason": "expand exact clock expression for natural Vietnamese TTS"},
    {"source": "01 giờ 37 phút", "spoken": "một giờ ba mươi bảy phút", "scope": ["narration"], "reason": "expand exact clock expression for natural Vietnamese TTS"},
    {"source": "03 giờ 11", "spoken": "ba giờ mười một phút", "scope": ["narration"], "reason": "expand exact clock expression for natural Vietnamese TTS"},
    {"source": "Audio", "spoken": "au đi ô", "scope": ["intro"], "reason": "brand pronunciation only; semantic intro unchanged"},
    {"source": "like", "spoken": "lai", "scope": ["intro"], "reason": "English CTA pronunciation only; semantic intro unchanged"},
    {"source": "video", "spoken": "vi đi ô", "scope": ["intro"], "reason": "English loanword pronunciation only; semantic intro unchanged"},
]
REVIEWED_INVENTORY = ["05 giờ 12 phút", "05 giờ 09", "01 giờ 37 phút", "03 giờ 11", "Audio", "like", "video"]


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


def sha_path(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(8 * 1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def atomic_bytes(path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    part = path.with_suffix(path.suffix + ".part")
    part.write_bytes(data)
    os.replace(part, path)


def atomic_json(path, value):
    atomic_bytes(path, (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8"))


def byte_offset(text, char_offset):
    return len(text[:char_offset].encode("utf-8"))


def pattern(source):
    return re.compile(r"(?<![\w])" + re.escape(source) + r"(?![\w])", flags=re.IGNORECASE | re.UNICODE)


def transform(text, scope):
    candidates = []
    for entry_index, entry in enumerate(LEXICON_ENTRIES):
        if scope not in entry["scope"]:
            continue
        for match in pattern(entry["source"]).finditer(text):
            candidates.append((match.start(), match.end(), entry_index, match.group(0), entry["spoken"]))
    candidates.sort(key=lambda row: (row[0], -(row[1] - row[0]), row[2]))
    selected = []
    cursor = -1
    for item in candidates:
        if item[0] < cursor:
            continue
        selected.append(item)
        cursor = item[1]

    pieces = []
    replacements = []
    source_cursor = 0
    output_chars = 0
    for start, end, entry_index, source_value, spoken in selected:
        unchanged = text[source_cursor:start]
        pieces.append(unchanged)
        output_chars += len(unchanged)
        out_start = output_chars
        pieces.append(spoken)
        output_chars += len(spoken)
        out_end = output_chars
        replacements.append({
            "scope": scope,
            "lexicon_index": entry_index,
            "source": source_value,
            "spoken": spoken,
            "source_char_span": [start, end],
            "source_byte_span": [byte_offset(text, start), byte_offset(text, end)],
            "output_char_span": [out_start, out_end],
        })
        source_cursor = end
    pieces.append(text[source_cursor:])
    output = "".join(pieces)
    for row in replacements:
        a, b = row["output_char_span"]
        row["output_byte_span"] = [byte_offset(output, a), byte_offset(output, b)]

    rebuilt = []
    cursor = 0
    for row in replacements:
        start, end = row["output_char_span"]
        rebuilt.append(output[cursor:start])
        rebuilt.append(row["source"])
        cursor = end
    rebuilt.append(output[cursor:])
    reconstructed = "".join(rebuilt)
    if reconstructed.encode("utf-8") != text.encode("utf-8"):
        raise RuntimeError(f"reverse reconstruction failed for {scope}")
    return output, replacements


def main():
    manifest_path = ROOT / "script/project-manifest.json"
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    if manifest.get("active_run") != "run-002" or manifest.get("steps", {}).get("story") != "completed":
        raise RuntimeError("pronunciation blocked until active run-002 Story Promotion Gate PASS")
    active = manifest["active_paths"]
    promotion_path = ROOT / active["promotion_receipt"]
    source_path = ROOT / active["spoken_narration_source"]
    canon_path = ROOT / active["story_canon"]
    tts_assembly_path = ROOT / active["tts_semantic_assembly_receipt"]
    output_path = ROOT / active["spoken_narration"]
    lexicon_path = ROOT / active["tts_pronunciation_lexicon"]
    receipt_path = ROOT / active["tts_pronunciation_receipt"]

    for output in (output_path, lexicon_path, receipt_path):
        if output.exists() or output.with_suffix(output.suffix + ".part").exists():
            raise RuntimeError(f"pronunciation output must be virgin: {output}")
    promotion = json.loads(promotion_path.read_text(encoding="utf-8"))
    if promotion.get("status") != "completed" or promotion.get("verified") is not True:
        raise RuntimeError("promotion not terminal PASS")
    source_bytes = source_path.read_bytes()
    canon_bytes = canon_path.read_bytes()
    source_sha = sha_bytes(source_bytes)
    canon_sha = sha_bytes(canon_bytes)
    if source_sha != promotion.get("spoken_narration_source_sha256") or canon_sha != promotion.get("source_canon_sha256"):
        raise RuntimeError("promotion source/canon hash drift")
    tts_assembly = json.loads(tts_assembly_path.read_text(encoding="utf-8"))
    if tts_assembly.get("status") != "PASS" or tts_assembly.get("verified") is not True:
        raise RuntimeError("TTS semantic assembly receipt not terminal PASS")
    if tts_assembly.get("semantic_source_sha256") != source_sha or tts_assembly.get("source_candidate_sha256") != canon_sha:
        raise RuntimeError("TTS semantic assembly lineage drift")
    if tts_assembly.get("separator_contract") != "exactly three blank lines between source parts":
        raise RuntimeError("TTS separator contract drift")
    part_bytes = []
    for row in tts_assembly.get("source_parts", []):
        path = ROOT / row["path"]
        raw = path.read_bytes()
        if sha_bytes(raw) != row["sha256"]:
            raise RuntimeError(f"TTS source part hash drift: {row['path']}")
        part_bytes.append(raw)
    if len(part_bytes) != 4 or b"\n\n\n".join(part_bytes) != source_bytes:
        raise RuntimeError("TTS semantic source reverse assembly failed")
    source_text = source_bytes.decode("utf-8")
    if unicodedata.normalize("NFC", source_text) != source_text:
        raise RuntimeError("narration source must be NFC")

    narration_text, narration_replacements = transform(source_text, "narration")
    intro_text, intro_replacements = transform(INTRO_SOURCE, "intro")
    remaining_ascii_digits = [
        {"char": match.group(0), "char_offset": match.start()}
        for match in re.finditer(r"[0-9]", narration_text)
    ]
    if remaining_ascii_digits:
        raise RuntimeError(f"spoken narration has unresolved ASCII digits: {remaining_ascii_digits[:20]}")
    inventory = []
    unresolved = []
    for index, entry in enumerate(LEXICON_ENTRIES):
        narration_count = len(pattern(entry["source"]).findall(source_text)) if "narration" in entry["scope"] else 0
        intro_count = len(pattern(entry["source"]).findall(INTRO_SOURCE)) if "intro" in entry["scope"] else 0
        required = "intro" in entry["scope"] or "narration" in entry["scope"]
        status = "resolved" if narration_count + intro_count > 0 else "unresolved"
        inventory.append({
            "lexicon_index": index, "source": entry["source"], "spoken": entry["spoken"],
            "scope": entry["scope"], "narration_occurrences": narration_count,
            "intro_occurrences": intro_count, "status": status,
        })
        if required and status != "resolved":
            unresolved.append(entry["source"])
    if unresolved:
        raise RuntimeError(f"lexicon entries have no occurrence: {unresolved}")
    expected_narration = sum(row["narration_occurrences"] for row in inventory)
    expected_intro = sum(row["intro_occurrences"] for row in inventory)
    if len(narration_replacements) != expected_narration or len(intro_replacements) != expected_intro:
        raise RuntimeError("pronunciation occurrence inventory drift")

    lexicon = {
        "status": "locked", "verified": True, "transform_version": TRANSFORM_VERSION,
        "matching": "exact Unicode token boundary; longest span first; no substring replacement",
        "entries": LEXICON_ENTRIES, "reviewed_foreign_inventory": REVIEWED_INVENTORY,
        "unresolved": [],
    }
    lexicon_bytes = (json.dumps(lexicon, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
    narration_bytes = narration_text.encode("utf-8")
    now = datetime.datetime.now(datetime.timezone.utc).isoformat()
    receipt = {
        "status": "completed", "verified": True, "transform_version": TRANSFORM_VERSION,
        "source_canon_sha256": canon_sha,
        "spoken_narration_source_path": active["spoken_narration_source"],
        "spoken_narration_source_sha256": source_sha,
        "spoken_narration_path": active["spoken_narration"],
        "spoken_narration_sha256": sha_bytes(narration_bytes),
        "lexicon_path": active["tts_pronunciation_lexicon"], "lexicon_sha256": sha_bytes(lexicon_bytes),
        "inventory": inventory, "replacements": narration_replacements,
        "replacement_count": len(narration_replacements), "outside_replacement_spans_unchanged": True,
        "reverse_reconstruction_verified": True, "unresolved_count": 0, "remaining_ascii_digits": 0,
        "intro": {
            "source_text": INTRO_SOURCE, "source_text_sha256": sha_bytes(INTRO_SOURCE.encode("utf-8")),
            "tts_text": intro_text, "tts_text_sha256": sha_bytes(intro_text.encode("utf-8")),
            "replacements": intro_replacements, "replacement_count": len(intro_replacements),
            "reverse_reconstruction_verified": True,
        },
        "completed_at": now,
    }
    atomic_bytes(lexicon_path, lexicon_bytes)
    atomic_bytes(output_path, narration_bytes)
    atomic_json(receipt_path, receipt)
    if sha_path(lexicon_path) != receipt["lexicon_sha256"] or sha_path(output_path) != receipt["spoken_narration_sha256"]:
        raise RuntimeError("pronunciation readback hash failed")

    manifest["status"] = "pronunciation_completed"
    manifest["spoken_narration_sha256"] = receipt["spoken_narration_sha256"]
    manifest["tts_pronunciation_lexicon_sha256"] = receipt["lexicon_sha256"]
    manifest["steps"]["pronunciation"] = "completed"
    manifest["pronunciation"] = {
        "status": "completed", "verified": True, "receipt": active["tts_pronunciation_receipt"],
        "source_sha256": source_sha, "output_sha256": receipt["spoken_narration_sha256"],
        "replacement_count": len(narration_replacements), "unresolved_count": 0, "completed_at": now,
    }
    manifest["updated_at"] = now
    atomic_json(manifest_path, manifest)
    print(json.dumps({"status": "completed", "replacements": len(narration_replacements), "intro_replacements": len(intro_replacements), "spoken_narration_sha256": receipt["spoken_narration_sha256"]}, ensure_ascii=False))


if __name__ == "__main__":
    main()
