"""Session 22: limited text audit and published Solitaire vectors.
Usage: python stephenson_audit.py SOURCE_DIR
SOURCE_DIR contains snow.html from the cited publisher and 2012.md, 2013.md,
2014.md from the cited archive. Copyrighted publisher text is not redistributed.
Only numerical results, hashes and bounded matching phrases are written.
"""
from pathlib import Path
import json,re,html,hashlib,sys
ROOT=Path(__file__).resolve().parent
R='ᚠᚢᚦᚩᚱᚳᚷᚹᚻᚾᛁᛄᛇᛈᛉᛋᛏᛒᛖᛗᛚᛝᛟᛞᚪᚫᚣᛡᛠ'
TERMS=['neal stephenson','stephenson','snow crash','cryptonomicon','pontifex','solitaire','enoch root','anathem','diamond age','baroque cycle','quicksilver','the system of the world','reamde']

def normalize(t):return re.findall('[a-z]+',t.lower())
def cut(d,n):return d[n:-1]+d[:n]+d[-1:]
def round_deck(d):
 for joker,steps in [(53,1),(54,2)]:
  for _ in range(steps):
   i=d.index(joker);d.pop(i);d.insert((i+1) if i<53 else 1,joker)
 a,b=sorted([d.index(53),d.index(54)]);d=d[b+1:]+d[a:b+1]+d[:a]
 return cut(d,min(d[-1],53))
def stream(key,n):
 d=list(range(1,55))
 for c in ''.join(normalize(key)).upper():d=cut(round_deck(d),ord(c)-64)
 out=[]
 while len(out)<n:
  d=round_deck(d);v=d[min(d[0],53)]
  if v<53:out.append((v-1)%26+1)
 return out
def encrypt(text,key):
 t=''.join(normalize(text)).upper();t+='X'*((-len(t))%5)
 return ''.join(chr((ord(c)-65+k)%26+65) for c,k in zip(t,stream(key,len(t))))

def main():
 src=Path(sys.argv[1]);checks=[]
 for key,plain,expected in [('', 'AAAAAAAAAA','EXKYIZSGEH'),('FOO','AAAAAAAAAAAAAAA','ITHZUJIWGRFARMW'),('CRYPTONOMICON','SOLITAIRE','KIRAKSFJAN')]:
  got=encrypt(plain,key);assert got==expected,(key,got)
  checks.append(dict(key=key,plaintext=plain,expected=expected,actual=got,passed=True))
 raw=(ROOT/'liber-primus__transcription--master.txt').read_text();pages=raw[raw.index('ᚱ-'):].split('%')[:73];lp=pages[15:]
 inv=[dict(lp2_page=i,runes=sum(c in R for c in p),distinct_runes=len(set(p)&set(R))) for i,p in enumerate(lp[:56])]
 # Search clauses separately: no false phrase created by joining different LP clauses.
 trans=(ROOT/'liber-primus__translation.txt').read_text()
 clauses=[normalize(m.group(1)) for l in trans.splitlines() if (m:=re.match(r'^\d+(?:\.\d+)+\s+(.+)$',l))]
 s=(src/'snow.html').read_text();m=re.search(r'var excerpt_text_list\s*=\s*',s);assert m
 d=json.JSONDecoder().raw_decode(s[m.end():])[0]
 excerpt=html.unescape(re.sub('<[^>]+>',' ',d['9780553380958'].split('<span class="excrpt_cp">')[0]));tokens=normalize(excerpt)
 matches={}
 for n in [4,5,6]:
  a={tuple(c[i:i+n]) for c in clauses for i in range(len(c)-n+1)};b={tuple(tokens[i:i+n]) for i in range(len(tokens)-n+1)}
  matches[str(n)]=[' '.join(x) for x in sorted(a&b)]
 archive=[]
 for y in [2012,2013,2014]:
  p=src/f'{y}.md';b=p.read_bytes();t=' '+' '.join(normalize(b.decode()))+' '
  archive.append(dict(year=y,url=f'https://raw.githubusercontent.com/scream314/cicada3301/master/{y}.md',sha256=hashlib.sha256(b).hexdigest(),bytes=len(b),hits={term:len(re.findall(r'(?<![a-z])'+re.escape(term)+r'(?![a-z])',t)) for term in TERMS}))
 out=dict(solitaire_source='https://www.schneier.com/academic/solitaire/',published_vectors=checks,lp2_unsolved_inventory=inv,snow_comparison=dict(url='https://www.penguinrandomhouse.com/books/172832/snow-crash-by-neal-stephenson/',edition_key='9780553380958',scope='Publisher opening excerpt only, not full novel',excerpt_sha256=hashlib.sha256(excerpt.encode()).hexdigest(),html_sha256=hashlib.sha256(s.encode()).hexdigest(),excerpt_tokens=len(tokens),lp_clauses=len(clauses),lp_tokens=sum(map(len,clauses)),matches=matches),archive_scan=archive)
 (ROOT/'stephenson_audit_results.json').write_text(json.dumps(out,indent=2)+'\n')
 print(json.dumps(dict(vectors_passed=len(checks),pages_over_26_symbols=sum(p['distinct_runes']>26 for p in inv),minimum_distinct=min(p['distinct_runes'] for p in inv),snow=out['snow_comparison'],archive_hits=[dict(year=a['year'],hits={k:v for k,v in a['hits'].items() if v}) for a in archive]),indent=2))
if __name__=='__main__':main()
