#!/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": "marathon", "spoken": "ma ra tông", "scope": ["narration"], "reason": "English event term"},
    {"source": "cabin", "spoken": "ca bin", "scope": ["narration"], "reason": "English loanword"},
    {"source": "camera", "spoken": "ca mê ra", "scope": ["narration"], "reason": "English loanword"},
    {"source": "inox", "spoken": "i nốc", "scope": ["narration"], "reason": "technical loanword"},
    {"source": "route", "spoken": "rút", "scope": ["narration"], "reason": "English logistics term"},
    {"source": "video", "spoken": "vi đi ô", "scope": ["narration", "intro"], "reason": "English loanword"},
    {"source": "Audio", "spoken": "au đi ô", "scope": ["intro"], "reason": "brand pronunciation only; canonical copy unchanged"},
    {"source": "like", "spoken": "lai", "scope": ["intro"], "reason": "English CTA pronunciation only; canonical copy unchanged"},
]


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 transform(text, scope):
    candidates = []
    for entry_index, entry in enumerate(LEXICON_ENTRIES):
        if scope not in entry["scope"]:
            continue
        pattern = re.compile(r"(?<![\w])" + re.escape(entry["source"]) + r"(?![\w])", flags=re.IGNORECASE | re.UNICODE)
        for match in pattern.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 count_term(text, source):
    pattern = re.compile(r"(?<![\w])" + re.escape(source) + r"(?![\w])", flags=re.IGNORECASE | re.UNICODE)
    return len(pattern.findall(text))


def main():
    manifest_path = ROOT / "script/project-manifest.json"
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    paths = manifest["active_paths"]
    promotion_path = ROOT / paths["promotion_receipt"]
    source_path = ROOT / paths["spoken_narration_source"]
    canon_path = ROOT / paths["story_canon"]
    output_path = ROOT / paths["spoken_narration"]
    lexicon_path = ROOT / paths["tts_pronunciation_lexicon"]
    receipt_path = ROOT / paths["tts_pronunciation_receipt"]

    for output in (output_path, lexicon_path, receipt_path):
        if output.exists():
            raise RuntimeError(f"pronunciation output must be virgin: {output}")
    promotion = json.loads(promotion_path.read_text(encoding="utf-8"))
    if promotion.get("verified") is not True or promotion.get("status") != "completed":
        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")
    if source_bytes != canon_bytes:
        raise RuntimeError("narration source must be byte-identical canon for this project")
    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")
    inventory = []
    for index, entry in enumerate(LEXICON_ENTRIES):
        narration_count = count_term(source_text, entry["source"]) if "narration" in entry["scope"] else 0
        intro_count = count_term(INTRO_SOURCE, entry["source"]) if "intro" in entry["scope"] else 0
        inventory.append({
            "lexicon_index": index,
            "source": entry["source"],
            "spoken": entry["spoken"],
            "scope": entry["scope"],
            "narration_occurrences": narration_count,
            "intro_occurrences": intro_count,
            "status": "resolved",
        })
    if not narration_replacements:
        raise RuntimeError("expected foreign-term replacements not found")
    for entry in inventory:
        if entry["narration_occurrences"] + entry["intro_occurrences"] == 0:
            raise RuntimeError(f"lexicon entry has no occurrence: {entry['source']}")

    lexicon = {
        "status": "locked",
        "transform_version": TRANSFORM_VERSION,
        "matching": "exact Unicode token boundary; longest span first; no substring replacement",
        "entries": LEXICON_ENTRIES,
        "reviewed_foreign_inventory": ["marathon", "cabin", "camera", "inox", "route", "video", "Audio", "like"],
        "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": paths["spoken_narration_source"],
        "spoken_narration_source_sha256": source_sha,
        "spoken_narration_path": paths["spoken_narration"],
        "spoken_narration_sha256": sha_bytes(narration_bytes),
        "lexicon_path": paths["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,
        "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": paths["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()
