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

PROJECT = Path(__file__).resolve().parents[1]
V3 = PROJECT / "story" / "v3" / "spoken-narration.txt"
REPORT = PROJECT / "story" / "v3" / "originality-report.json"
WORD_RE = re.compile(r"[\wÀ-ỹĐđ]+", re.UNICODE)
SHINGLE_SIZE = 14


def normalize(text):
    text = unicodedata.normalize("NFC", text).lower()
    return WORD_RE.findall(text)


def shingles(tokens, size=SHINGLE_SIZE):
    return {tuple(tokens[index:index + size]) for index in range(max(0, len(tokens) - size + 1))}


def normalized_paragraph(paragraph):
    return " ".join(normalize(paragraph))


def find_rejected_authority():
    archives = sorted((PROJECT / "archive").glob("superseded-patchwork-*/story/spoken-narration.txt"))
    return archives[-1] if archives else None


def duplicate_paragraphs(text):
    seen = {}
    duplicates = []
    paragraphs = [item.strip() for item in re.split(r"\n\s*\n", text) if item.strip()]
    for index, paragraph in enumerate(paragraphs, 1):
        key = normalized_paragraph(paragraph)
        if len(key.split()) < 25:
            continue
        if key in seen:
            duplicates.append({
                "first_paragraph": seen[key],
                "duplicate_paragraph": index,
                "excerpt": paragraph[:240],
            })
        else:
            seen[key] = index
    return duplicates


def longest_common_run(new_tokens, old_tokens):
    # Rolling lookup finds the longest exact token run without quadratic full-text comparison.
    old_positions = {}
    for index in range(max(0, len(old_tokens) - SHINGLE_SIZE + 1)):
        key = tuple(old_tokens[index:index + SHINGLE_SIZE])
        old_positions.setdefault(key, []).append(index)

    best = (0, 0, 0)
    for new_index in range(max(0, len(new_tokens) - SHINGLE_SIZE + 1)):
        key = tuple(new_tokens[new_index:new_index + SHINGLE_SIZE])
        for old_index in old_positions.get(key, [])[:20]:
            length = SHINGLE_SIZE
            while (
                new_index + length < len(new_tokens)
                and old_index + length < len(old_tokens)
                and new_tokens[new_index + length] == old_tokens[old_index + length]
            ):
                length += 1
            if length > best[0]:
                best = (length, new_index, old_index)
    return best


def main():
    issues = []
    if not V3.exists():
        raise SystemExit(f"Missing v3 narration: {V3}")
    rejected = find_rejected_authority()
    if rejected is None:
        raise SystemExit("Rejected v2 narration archive not found")

    new_text = V3.read_text(encoding="utf-8")
    old_text = rejected.read_text(encoding="utf-8")
    new_tokens = normalize(new_text)
    old_tokens = normalize(old_text)
    new_shingles = shingles(new_tokens)
    old_shingles = shingles(old_tokens)
    shared = new_shingles & old_shingles
    shared_ratio = len(shared) / max(1, len(new_shingles))
    longest, new_start, old_start = longest_common_run(new_tokens, old_tokens)
    duplicates = duplicate_paragraphs(new_text)

    # Proper names, fixed title concepts and short connective phrases can overlap. Exact runs
    # longer than 35 words or broad 14-word shingle overlap above 0.5% require manual review.
    if longest > 35:
        issues.append(f"long_exact_run_with_rejected_v2:{longest}_words")
    if shared_ratio > 0.005:
        issues.append(f"shared_14_word_shingle_ratio_too_high:{shared_ratio:.6f}")
    if duplicates:
        issues.append(f"duplicate_paragraphs_inside_v3:{len(duplicates)}")

    shared_examples = [" ".join(item) for item in sorted(shared)[:20]]
    longest_excerpt = " ".join(new_tokens[new_start:new_start + min(longest, 80)]) if longest else ""
    report = {
        "version": 3,
        "verified": not issues,
        "status": "passed" if not issues else "failed",
        "source_canon_sha256": hashlib.sha256(V3.read_bytes()).hexdigest(),
        "v3_words": len(new_tokens),
        "rejected_v2_path": str(rejected.relative_to(PROJECT)),
        "rejected_v2_sha256": hashlib.sha256(rejected.read_bytes()).hexdigest(),
        "rejected_v2_words": len(old_tokens),
        "shingle_size_words": SHINGLE_SIZE,
        "v3_unique_shingles": len(new_shingles),
        "shared_shingles": len(shared),
        "shared_shingle_ratio": shared_ratio,
        "longest_exact_run_words": longest,
        "longest_exact_run_excerpt": longest_excerpt,
        "shared_examples": shared_examples,
        "duplicate_paragraphs": duplicates,
        "issues": issues,
        "checked_at": datetime.now(timezone.utc).isoformat(),
    }
    REPORT.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({
        "verified": report["verified"],
        "v3_words": report["v3_words"],
        "shared_shingle_ratio": round(shared_ratio, 8),
        "longest_exact_run_words": longest,
        "duplicate_paragraphs": len(duplicates),
        "issues": issues,
        "report": str(REPORT),
    }, ensure_ascii=False))
    return 0 if report["verified"] else 1


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