#!/usr/bin/env python3
import hashlib
import json
import re
import sys
from pathlib import Path

PROJECT_ID = "gacmai_20260716_233054"
WORD_MIN = 14400
WORD_MAX = 15000
META_PATTERNS = [
    r"câu chuyện (?:kết thúc|dừng lại)",
    r"hết phần \d+",
    r"phần tiếp theo",
    r"không còn phần sau",
]

def words(text):
    return len(re.findall(r"\b\w+\b", text, re.UNICODE))

def fail(errors):
    print(json.dumps({"status":"failed","errors":errors}, ensure_ascii=False, indent=2))
    raise SystemExit(1)

def main(story_path, outline_path):
    sp, op = Path(story_path), Path(outline_path)
    story, outline = json.loads(sp.read_text()), json.loads(op.read_text())
    errors=[]
    if story.get("project_id") != PROJECT_ID: errors.append("project identity mismatch")
    if story.get("title") != outline.get("title"): errors.append("title mismatch")
    expected_outline_sha=hashlib.sha256(op.read_bytes()).hexdigest()
    if story.get("outline_sha256") != expected_outline_sha: errors.append("outline checksum mismatch")
    parts=story.get("parts")
    if not isinstance(parts,list) or [p.get("part") for p in parts] != list(range(1,13)):
        errors.append("parts must be exactly 1-12")
        parts=[]
    narration=[]
    for p in parts:
        paras=p.get("paragraphs")
        if not isinstance(paras,list) or len(paras)!=12 or any(not isinstance(x,str) or not x.strip() for x in paras):
            errors.append(f"part {p.get('part')} must contain 12 nonempty paragraph strings")
            continue
        narration.extend(paras)
    text="\n\n".join(narration)
    count=words(text)
    if not WORD_MIN <= count <= WORD_MAX: errors.append(f"word count {count} outside {WORD_MIN}-{WORD_MAX}")
    cast=outline.get("cast",[])
    for name in cast:
        if name not in text: errors.append(f"cast missing from narration: {name}")
    # Detect named-person inventory supplied by writer; exact identity must match outline.
    if story.get("named_characters") != cast: errors.append("named_characters must exactly equal locked cast in order")
    for name in cast:
        surname=name.split()[0]
        # Full-name occurrences are removed before checking bare surname.
        scrub=text.replace(name, "")
        if re.search(rf"(?<!\w){re.escape(surname)}(?!\w)", scrub):
            errors.append(f"bare surname detected: {surname}")
    for pat in META_PATTERNS:
        if re.search(pat,text,re.I): errors.append(f"meta prose detected: {pat}")
    if story.get("evidence_ids") != [f"E{i:02d}" for i in range(1,10)]: errors.append("evidence_ids must be E01-E09")
    if story.get("pov") != "limited_to_female_protagonist": errors.append("POV mismatch")
    if not story.get("closed_ending",False): errors.append("closed_ending must be true")
    if errors: fail(errors)
    out={"status":"passed","story_sha256":hashlib.sha256(sp.read_bytes()).hexdigest(),"outline_sha256":expected_outline_sha,"parts":12,"paragraphs":len(narration),"words":count,"cast":cast,"evidence_ids":story["evidence_ids"]}
    print(json.dumps(out,ensure_ascii=False))

if __name__=="__main__":
    if len(sys.argv)!=3: fail(["usage: validator STORY.json OUTLINE.json"])
    main(sys.argv[1],sys.argv[2])
