#!/usr/bin/env python3
import json,re,sys,hashlib,collections
from pathlib import Path
PROJECT='gacmai_20260718_091744'
CAST={'Nguyễn Hoài Lan','Lê Thu Hạnh','Vũ Minh Thư','Trần Minh Quang','Phạm Quốc Dũng','Đỗ Văn Phúc'}
FORBIDDEN=[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',r'an toàn thực phẩm|ngộ độc|phá hoại|truy cập trái phép|gian lận kho vận']
REQ_BEAT=['global_ordinal','part','paragraph','scene','pov','characters','goal','conflict','action','causal_result','evidence_state','consent_privacy','authority_limit','end_state']

def norm(s):return re.sub(r'\s+',' ',str(s).strip().lower())
def main():
 if len(sys.argv)!=3:raise SystemExit('usage: validate_outline.py FILE CONCEPT_SHA')
 p=Path(sys.argv[1]);raw=p.read_bytes();d=json.loads(raw);expected=sys.argv[2];errors=[]
 if d.get('project_id')!=PROJECT:errors.append('project_id')
 if d.get('source_concept_sha256')!=expected:errors.append('concept_sha')
 if set(d.get('cast',[]))!=CAST:errors.append('exact_cast')
 if d.get('sole_pov')!='Nguyễn Hoài Lan':errors.append('sole_pov')
 parts=d.get('parts',[])
 if len(parts)!=12:errors.append('part_count')
 beats=[]
 for pi,part in enumerate(parts,1):
  if part.get('part')!=pi:errors.append(f'part_number:{pi}')
  bs=part.get('beats',[])
  if len(bs)!=12:errors.append(f'beat_count:{pi}')
  for qi,b in enumerate(bs,1):
   beats.append(b)
   for k in REQ_BEAT:
    if k not in b or b[k] in (None,'',[]):errors.append(f'missing:{pi}.{qi}:{k}')
   if b.get('part')!=pi or b.get('paragraph')!=qi or b.get('global_ordinal')!=(pi-1)*12+qi:errors.append(f'ordinal:{pi}.{qi}')
   if b.get('pov')!='Nguyễn Hoài Lan':errors.append(f'pov:{pi}.{qi}')
   chars=set(b.get('characters',[]))
   if not chars.issubset(CAST):errors.append(f'extra_cast:{pi}.{qi}')
 if len(beats)!=144:errors.append('total_beats')
 ords=[b.get('global_ordinal') for b in beats]
 if ords!=list(range(1,145)):errors.append('global_ordinals')
 text=json.dumps({'parts':parts},ensure_ascii=False).lower()
 hits=[]
 for pat in FORBIDDEN:
  m=re.search(pat,text,re.I)
  if m:hits.append(m.group(0))
 if hits:errors.append('forbidden:'+','.join(sorted(set(hits))))
 # Reject repeated semantic scaffold in fields that should advance causality.
 for field in ('goal','conflict','action','causal_result','consent_privacy','authority_limit','end_state'):
  c=collections.Counter(norm(b.get(field,'')) for b in beats)
  bad={k:v for k,v in c.items() if k and v>2}
  if bad:errors.append(f'repeated_{field}:'+str(max(bad.values())))
 # Ending must remain conditional and cannot claim implementation success before aftercare.
 ending=' '.join(str(beats[i].get(k,'')) for i in range(132,144) for k in ('action','causal_result','end_state')).lower() if len(beats)==144 else ''
 if not any(x in ending for x in ('điều kiện','nếu','rà soát','mốc','kiểm tra','thanh toán')):errors.append('conditional_ending_missing')
 out={'status':'passed' if not errors else 'failed','source':str(p),'source_sha256':hashlib.sha256(raw).hexdigest(),'concept_sha256':expected,'parts':len(parts),'beats':len(beats),'errors':errors}
 print(json.dumps(out,ensure_ascii=False,indent=2))
 if errors:raise SystemExit(1)
if __name__=='__main__':main()
