#!/usr/bin/env python3
from __future__ import annotations

import hashlib
import json
import re
import sys
import unicodedata
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CANDIDATE = ROOT / "work/story/run-001/story-candidate.txt"
REPORT = ROOT / "log/candidate-validation.json"
CORPUS = ROOT / "log/originality-corpus.json"
MIN_WORDS = 9700
MAX_WORDS = 12500


def sha256_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def normalize(text: str) -> str:
    text = unicodedata.normalize("NFC", text).casefold()
    return " ".join(text.split())


def prose_from_json(value: object, key: str = "") -> list[str]:
    result: list[str] = []
    if isinstance(value, str):
        if len(value) >= 120 and key.casefold() in {
            "text", "content", "narration", "story", "paragraph", "body", "prose"
        }:
            result.append(value)
    elif isinstance(value, list):
        for item in value:
            result.extend(prose_from_json(item, key))
    elif isinstance(value, dict):
        for child_key, item in value.items():
            result.extend(prose_from_json(item, child_key))
    return result


def load_corpus_text(path: Path) -> str:
    if path.suffix.casefold() == ".txt":
        return path.read_text(encoding="utf-8")
    obj = json.loads(path.read_text(encoding="utf-8"))
    return "\n\n".join(prose_from_json(obj))


def ngrams(tokens: list[str], size: int) -> set[tuple[str, ...]]:
    if len(tokens) < size:
        return set()
    return {tuple(tokens[i:i + size]) for i in range(len(tokens) - size + 1)}


def main() -> int:
    if not CANDIDATE.exists():
        print(json.dumps({"status": "blocked", "reason": "candidate_missing", "path": str(CANDIDATE)}))
        return 2

    raw = CANDIDATE.read_bytes()
    text = raw.decode("utf-8", errors="strict")
    lines = text.splitlines()
    words = text.split()

    findings: dict[str, object] = {
        "markdown_heading_lines": [
            i + 1 for i, line in enumerate(lines)
            if re.match(r"^\s*#{1,6}\s+\S", line)
        ],
        "scene_marker_lines": [
            i + 1 for i, line in enumerate(lines)
            if re.match(
                r"^\s*(?:(?:CHƯƠNG|Chương|PHẦN|Phần|CẢNH|Cảnh|SCENE|Scene|ACT|Act)"
                r"(?:\s+\d+|\s*[:.-])|(?:\*\s*\*\s*\*|---+|===+))\s*$",
                line,
            )
        ],
        "json_structure_lines": [
            i + 1 for i, line in enumerate(lines)
            if re.match(r"^\s*[\[\]{}]\s*,?\s*$", line)
            or re.match(r'^\s*"[^"\n]+"\s*:', line)
        ],
        "meta_note_lines": [
            i + 1 for i, line in enumerate(lines)
            if re.match(
                r"^\s*(?:Ghi chú|Lưu ý|Meta|Narration|Tóm tắt|Từ khóa|Word count|SHA-?256)\s*:",
                line,
                re.I,
            )
        ],
        "placeholder_lines": [
            i + 1 for i, line in enumerate(lines)
            if re.search(r"\b(?:TODO|TBD|PLACEHOLDER)\b|<[^>]{2,50}>", line, re.I)
        ],
        "channel_cta_lines": [
            i + 1 for i, line in enumerate(lines)
            if re.search(r"Gác Mái Audio|like video|đăng ký kênh", line, re.I)
        ],
    }

    required_names = ["Tống Vãn Ninh", "Cố Trạch Xuyên", "Lục Mạn Thanh", "Tần Nhược Lan", "Tống Nghi Lan"]
    required_concepts = {
        "wedding_dress": ["áo cưới", "lụa ngà"],
        "red_thread": ["chỉ đỏ", "đỏ son"],
        "workshop": ["Túc Vũ", "xưởng may"],
        "restoration": ["phục chế"],
        "consent": ["xin phép", "quyền từ chối", "tự chọn", "quyết định"],
        "ending": ["mùa mưa sau", "cầu hôn", "chiếc chìa"],
    }
    missing_names = [name for name in required_names if name not in text]
    missing_concepts = [
        key for key, aliases in required_concepts.items()
        if not any(alias.casefold() in text.casefold() for alias in aliases)
    ]

    stripped = text
    for name in required_names:
        stripped = stripped.replace(name, " ")
    prohibited_single_name_calls = []
    for token in ("Ninh", "Xuyên", "Thanh", "Lan"):
        matches = list(re.finditer(rf"(?<!\w){re.escape(token)}(?!\w)", stripped))
        if matches:
            prohibited_single_name_calls.append({"token": token, "count": len(matches)})

    candidate_tokens = normalize(text).split()
    candidate_ngrams = ngrams(candidate_tokens, 12)
    originality = []
    corpus = json.loads(CORPUS.read_text(encoding="utf-8"))
    for item in corpus.get("corpus", []):
        source = Path(item["source_path"])
        if not source.exists():
            originality.append({"project": item["project"], "source_missing": True})
            continue
        old_text = load_corpus_text(source)
        old_tokens = normalize(old_text).split()
        old_ngrams = ngrams(old_tokens, 12)
        overlap = candidate_ngrams & old_ngrams
        originality.append({
            "project": item["project"],
            "source_path": str(source),
            "source_sha256": sha256_bytes(old_text.encode("utf-8")),
            "shared_12grams": len(overlap),
            "sample": [" ".join(value) for value in sorted(overlap)[:5]],
            "verified": len(overlap) == 0,
        })

    checks = {
        "utf8_strict": {"verified": True, "value": True},
        "word_count": {"verified": MIN_WORDS <= len(words) <= MAX_WORDS, "value": len(words)},
        "non_empty": {"verified": bool(text.strip()), "value": len(raw)},
        "narration_hygiene": {
            "verified": not any(findings.values()),
            "findings": findings,
        },
        "required_names": {"verified": not missing_names, "findings": missing_names},
        "required_concepts": {"verified": not missing_concepts, "findings": missing_concepts},
        "name_calling_rule": {
            "verified": not prohibited_single_name_calls,
            "findings": prohibited_single_name_calls,
        },
        "originality_12gram": {
            "verified": all(item.get("verified", False) for item in originality),
            "findings": originality,
        },
    }
    verified = all(value["verified"] for value in checks.values())
    report = {
        "status": "completed" if verified else "failed",
        "verified": verified,
        "candidate_path": str(CANDIDATE),
        "candidate_sha256": sha256_bytes(raw),
        "bytes": len(raw),
        "words_whitespace": len(words),
        "checks": checks,
    }
    REPORT.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({
        "status": report["status"],
        "verified": verified,
        "candidate_sha256": report["candidate_sha256"],
        "words_whitespace": len(words),
        "report": str(REPORT),
    }, ensure_ascii=False))
    return 0 if verified else 1


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