#!/usr/bin/env python3
import json,re,sys,hashlib,unicodedata
from pathlib import Path
p=Path(sys.argv[1]);d=json.loads(p.read_text())
def norm(s):
 s=unicodedata.normalize('NFC',s).lower();s=re.sub(r'[^\w\s]',' ',s,flags=re.UNICODE);return ' '.join(s.split())
def txt(x):return x if isinstance(x,str) else x.get('text') or x.get('narration') or ''
paras=[]
for part in d.get('parts',[]):
 for i,x in enumerate(part.get('paragraphs',[]),1):paras.append({'loc':f"P{part.get('part')}.{i}",'text':txt(x)})
sents=[]
for q in paras:
 for s in re.split(r'(?<=[.!?…])\s+',q['text']):
  n=norm(s)
  if len(n.split())>=8:sents.append((q['loc'],n))
def groups(items):
 by={}
 for loc,n in items:by.setdefault(n,[]).append(loc)
 return [{'text':n,'locations':locs} for n,locs in by.items() if len(locs)>1]
exact_sent=groups(sents);exact_para=groups([(q['loc'],norm(q['text'])) for q in paras])
ngrams={12:{},16:{}}
for q in paras:
 w=norm(q['text']).split()
 for n in ngrams:
  for i in range(len(w)-n+1):ngrams[n].setdefault(' '.join(w[i:i+n]),set()).add(q['loc'])
rep={str(n):[{'ngram':g,'locations':sorted(v)} for g,v in tab.items() if len(v)>1] for n,tab in ngrams.items()}
# Jaccard on 5-gram sets; only flag paragraphs with substantial shared structure.
sets=[]
for q in paras:
 w=norm(q['text']).split();sets.append((q['loc'],set(tuple(w[i:i+5]) for i in range(max(0,len(w)-4)))))
near=[]
for i,(la,a) in enumerate(sets):
 for lb,b in sets[i+1:]:
  if not a or not b:continue
  j=len(a&b)/len(a|b)
  if j>=0.55:near.append({'a':la,'b':lb,'jaccard':round(j,4)})
viol=len(exact_sent)+len(exact_para)+sum(len(x) for x in rep.values())+len(near)
out={'status':'passed' if viol==0 else 'failed','story_sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'paragraphs':len(paras),'sentences':len(sents),'exact_sentence_duplicates':exact_sent,'exact_paragraph_duplicates':exact_para,'repeated_ngrams':rep,'near_duplicates':near,'violation_count':viol};print(json.dumps(out,ensure_ascii=False));raise SystemExit(1 if viol else 0)
