#!/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]
CANON = PROJECT / "story/story-canon.txt"
SOURCE = PROJECT / "story/spoken-narration.txt"
OUTPUT = PROJECT / "story/tts-pronunciation.txt"
RECEIPT = PROJECT / "log/tts-pronunciation.json"

LEXICON = [
    {"source": "audio", "spoken": "au đi ô", "required": False},
    {"source": "vali", "spoken": "va li", "required": True},
]


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


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


def case_spoken(source_text, spoken):
    if source_text.isupper():
        return spoken.upper()
    if source_text[:1].isupper() and source_text[1:].islower():
        return spoken[:1].upper() + spoken[1:]
    return spoken


def main():
    if not CANON.is_file() or not SOURCE.is_file():
        raise RuntimeError("canon or spoken narration missing")
    source_bytes = SOURCE.read_bytes()
    canon_bytes = CANON.read_bytes()
    if source_bytes != canon_bytes:
        raise RuntimeError("spoken narration is not byte-identical to canon")
    text = source_bytes.decode("utf-8")
    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)
    pattern = re.compile(rf"(?<!\w)(?:{alternatives})(?!\w)", re.IGNORECASE)

    parts = []
    occurrences = []
    source_cursor = 0
    output_cursor = 0
    counts = {row["source"]: 0 for row in ordered}
    for match in pattern.finditer(text):
        unchanged = text[source_cursor:match.start()]
        parts.append(unchanged)
        output_cursor += len(unchanged)
        source_text = match.group(0)
        row = by_source[source_text.casefold()]
        spoken = case_spoken(source_text, row["spoken"])
        line = text.count("\n", 0, match.start()) + 1
        occurrences.append({
            "source": source_text,
            "spoken": spoken,
            "source_start": match.start(),
            "source_end": match.end(),
            "output_start": output_cursor,
            "output_end": output_cursor + len(spoken),
            "line": line,
        })
        counts[row["source"]] += 1
        parts.append(spoken)
        output_cursor += len(spoken)
        source_cursor = match.end()
    parts.append(text[source_cursor:])
    projected = "".join(parts)

    # Reverse replacements by recorded output offsets; this proves that only allowlisted
    # orthographic substitutions separate the projection from the canonical narration.
    restored = projected
    for item in reversed(occurrences):
        start, end = item["output_start"], item["output_end"]
        if restored[start:end] != item["spoken"]:
            raise RuntimeError("pronunciation reverse verification offset mismatch")
        restored = restored[:start] + item["source"] + restored[end:]
    reverse_verified = restored == text
    if not reverse_verified:
        raise RuntimeError("pronunciation reverse verification failed")

    remaining = {}
    lexicon_receipt = []
    for row in ordered:
        regex = re.compile(rf"(?<!\w){re.escape(row['source'])}(?!\w)", re.IGNORECASE)
        remaining_count = len(regex.findall(projected))
        remaining[row["source"]] = remaining_count
        if row["required"] and counts[row["source"]] == 0:
            raise RuntimeError(f"required pronunciation token absent from source: {row['source']}")
        if row["required"] and remaining_count != 0:
            raise RuntimeError(f"required pronunciation token remains: {row['source']}")
        lexicon_receipt.append({**row, "replacement_count": counts[row["source"]], "remaining_count": remaining_count})

    output_bytes = projected.encode("utf-8")
    temp = OUTPUT.with_suffix(".part.txt")
    temp.write_bytes(output_bytes)
    temp.replace(OUTPUT)
    verified = bool(output_bytes) and reverse_verified and sum(counts.values()) == len(occurrences)
    receipt = {
        "version": 1,
        "verified": verified,
        "status": "completed" if verified else "failed",
        "source_canon_path": str(CANON.relative_to(PROJECT)),
        "source_canon_sha256": sha_file(CANON),
        "source_path": str(SOURCE.relative_to(PROJECT)),
        "source_sha256": sha_file(SOURCE),
        "output_path": str(OUTPUT.relative_to(PROJECT)),
        "output_sha256": sha_file(OUTPUT),
        "lexicon_strategy": "deterministic_longest_match_first_unicode_word_boundary_case_aware",
        "lexicon": lexicon_receipt,
        "occurrences": occurrences,
        "replacement_count": len(occurrences),
        "reverse_verified": reverse_verified,
        "semantic_content_changed": False,
        "non_lexicon_changes": 0,
        "checked_at": datetime.now(timezone.utc).isoformat(),
    }
    RECEIPT.parent.mkdir(parents=True, exist_ok=True)
    RECEIPT.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({
        "verified": verified,
        "source_sha256": receipt["source_sha256"],
        "output_sha256": receipt["output_sha256"],
        "replacement_count": len(occurrences),
        "counts": counts,
    }, ensure_ascii=False))
    return 0 if verified else 1


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