#!/usr/bin/env python3
import json,re,sys,hashlib,collections
from pathlib import Path
PROJECT='gacmai_20260718_091744'
WORD_RE=re.compile(r"[0-9A-Za-zÀ-ỹĐđ]+(?:[-'][0-9A-Za-zÀ-ỹĐđ]+)*",re.UNICODE)
FORBIDDEN_META=[r'\bphần\s+\d+\b',r'\bchương\s+\d+\b',r'#+\s',r'\bbeat\b',r'\bscaffold\b',r'\bplaceholder\b',r'\blớp thứ (?:nhất|hai|ba)\b']
FORBIDDEN_DOMAIN=[r'bãi bồi|thủy lợi|nguồn nước|kiến trúc|trùng tu|bệnh viện|y khoa|dầu khí|xăng dầu',r'radio|băng ghi âm|cassette|ca khúc|bài hát|siêu nhiên|livestream']
def norm(s):return re.sub(r'\s+',' ',s.strip().lower())
def main():
 if len(sys.argv)!=4:raise SystemExit('usage: validate_story.py FILE OUTLINE_SHA TITLE')
 p=Path(sys.argv[1]);raw=p.read_bytes();d=json.loads(raw);outline=sys.argv[2];title=sys.argv[3];errors=[]
 if d.get('project_id')!=PROJECT:errors.append('project_id')
 if d.get('title')!=title:errors.append('title')
 if d.get('source_outline_sha256')!=outline:errors.append('outline_sha')
 parts=d.get('parts',[])
 if len(parts)!=12:errors.append('part_count')
 paras=[]
 for pi,part in enumerate(parts,1):
  if part.get('part')!=pi:errors.append(f'part_number:{pi}')
  ps=part.get('paragraphs',[])
  if len(ps)!=12:errors.append(f'paragraph_count:{pi}')
  for qi,q in enumerate(ps,1):
   if q.get('ordinal')!=(pi-1)*12+qi:errors.append(f'ordinal:{pi}.{qi}')
   text=q.get('text','').strip()
   if not text:errors.append(f'empty:{pi}.{qi}')
   paras.append(text)
 if len(paras)!=144:errors.append('total_paragraphs')
 words=sum(len(WORD_RE.findall(x)) for x in paras)
 if not 14000<=words<=15000:errors.append(f'word_count:{words}')
 c=collections.Counter(norm(x) for x in paras)
 dup=[k for k,v in c.items() if k and v>1]
 if dup:errors.append(f'exact_duplicate_paragraphs:{len(dup)}')
 text='\n'.join(paras)
 for pat in FORBIDDEN_META:
  if re.search(pat,text,re.I):errors.append('meta:'+pat)
 for pat in FORBIDDEN_DOMAIN:
  if re.search(pat,text,re.I):errors.append('forbidden_domain:'+pat)
 # Narration package must identify exact six-cast and one focal character.
 cast=d.get('cast',[])
 if len(cast)!=6 or len(set(cast))!=6:errors.append('cast')
 if d.get('sole_pov') not in cast:errors.append('sole_pov')
 # Evidence of first-person narration is review-only; dialogue may contain tôi/ta.
 first_person=[i+1 for i,x in enumerate(paras) if re.search(r'(^|[.!?]\s+)(tôi|ta)\s',x,re.I)]
 out={'status':'passed' if not errors else 'failed','source':str(p),'source_sha256':hashlib.sha256(raw).hexdigest(),'outline_sha256':outline,'parts':len(parts),'paragraphs':len(paras),'word_count':words,'exact_duplicate_count':len(dup),'first_person_review_ordinals':first_person,'errors':errors}
 print(json.dumps(out,ensure_ascii=False,indent=2))
 if errors:raise SystemExit(1)
if __name__=='__main__':main()
