#!/usr/bin/env python3
import json,hashlib,re,sys
from pathlib import Path
from collections import Counter
PROJECT='gacmai_20260718_075153'
CAST=['Võ Thanh Hà','Nguyễn Hữu Duy','Trần Mộc Lan','Lê Quang Vinh','Phạm Ngọc Thư','Đỗ Minh Khang']
SCAFFOLD=('P01B','P02B','P03B','P04B','P05B','P06B','P07B','P08B','P09B','P10B','P11B','P12B','nhịp kế tiếp','dòng ghi riêng','placeholder','todo','ghi chú biên tập')
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
def words(s):return len(re.findall(r"\b\w+\b",s,flags=re.UNICODE))
def main():
 if len(sys.argv)!=3:raise SystemExit('usage: validate_story.py STORY EXPECTED_OUTLINE_SHA')
 p=Path(sys.argv[1]);outline_sha=sys.argv[2];d=json.loads(p.read_text());err=[]
 if d.get('project_id')!=PROJECT:err.append('project_id')
 if d.get('source_outline_sha256')!=outline_sha:err.append('outline_binding')
 parts=d.get('parts',[]);paras=[]
 if len(parts)!=12:err.append(f'parts={len(parts)}')
 for i,part in enumerate(parts,1):
  q=part.get('paragraphs',[])
  if len(q)!=12:err.append(f'part_{i}_paragraphs={len(q)}')
  for x in q:
   if isinstance(x,dict):paras.append(str(x.get('text','')))
   else:paras.append(str(x))
 total=sum(words(x) for x in paras)
 if not 14000<=total<=15000:err.append(f'words={total}')
 if len(paras)!=144:err.append(f'paragraphs={len(paras)}')
 first_person=sum(len(re.findall(r'(?<!\w)(?:tôi|chúng\s+tôi)(?!\w)',x,re.I)) for x in paras)
 # Dialogue may legitimately use first person; semantic audit decides narrator POV.
 blob='\n'.join(paras)
 for s in SCAFFOLD:
  if s.lower() in blob.lower():err.append(f'scaffold:{s}')
 # Reject any capitalized full name not in the locked cast; institutional terms are excluded.
 candidates=set(re.findall(r'\b[A-ZÀ-ỸĐ][a-zà-ỹ]+(?:\s+[A-ZÀ-ỸĐ][a-zà-ỹ]+){1,3}\b',blob))
 allowed=set(CAST)|{'Hội đồng Rà soát Huyện','Hội đồng rà soát huyện'}
 extras=sorted(x for x in candidates if x not in allowed and not x.startswith(('Võ Thanh Hà','Nguyễn Hữu Duy','Trần Mộc Lan','Lê Quang Vinh','Phạm Ngọc Thư','Đỗ Minh Khang')))
 # Keep this heuristic as evidence only; prose capitalized phrases can be false positives.
 norm=[re.sub(r'\s+',' ',x.lower()).strip() for x in paras]
 exact_dups=[k for k,v in Counter(norm).items() if v>1]
 if exact_dups:err.append(f'exact_duplicate_paragraphs={len(exact_dups)}')
 out={'status':'passed' if not err else 'failed','project_id':PROJECT,'story_path':str(p),'story_sha256':sha(p),'outline_sha256':outline_sha,'parts':len(parts),'paragraphs':len(paras),'words':total,'first_person':first_person,'exact_duplicate_paragraphs':len(exact_dups),'named_phrase_candidates_for_review':extras[:100],'errors':err}
 print(json.dumps(out,ensure_ascii=False,indent=2));raise SystemExit(0 if not err else 1)
if __name__=='__main__':main()
