"""Session 24: compare user-supplied book screenshots with solved LP wording.
Requires Tesseract on PATH; otherwise standard-library Python.
Usage: python screenshot_phrase_audit.py UPLOAD_DIR SCRATCH_OCR_DIR
OCR text stays in SCRATCH_OCR_DIR, outside the published evidence directory.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from collections import defaultdict
import hashlib, json, os, re, subprocess, sys

ROOT = Path(__file__).resolve().parent

def tokens(text):
    # Rejoin printed words broken across lines, including soft hyphens.
    text = re.sub(r'([A-Za-z])[-\u00ad]\s*\n\s*([A-Za-z])', r'\1\2', text)
    return re.findall(r'[a-z]+', text.lower())

def grams(ts, n):
    return {tuple(ts[i:i+n]) for i in range(len(ts)-n+1)}

def main():
    upload, cache = map(Path, sys.argv[1:3]); cache.mkdir(parents=True, exist_ok=True)
    manifest = json.loads((ROOT/'stephenson_screenshot_manifest.json').read_text())
    translation = (ROOT/'liber-primus__translation.txt').read_text()
    entries = [(m.group(1), tokens(m.group(2))) for line in translation.splitlines()
               if (m:=re.match(r'^(\d+(?:\.\d+)+)\s+(.+)$', line))]
    def ocr(row):
        p = upload/row['filename']
        assert hashlib.sha256(p.read_bytes()).hexdigest()==row['sha256'], p.name
        output = cache/(p.stem+'.txt')
        if not output.exists():
            r = subprocess.run(['tesseract', str(p), 'stdout', '--psm', '6'],
                               text=True, capture_output=True, check=True,
                               env={**os.environ, 'OMP_THREAD_LIMIT':'1'})
            output.write_text(r.stdout)
        txt=output.read_text()
        return row, txt, tokens(txt)
    with ThreadPoolExecutor(max_workers=2) as pool:
        documents = list(pool.map(ocr, manifest['screenshots']))
    lp_index = {n:defaultdict(set) for n in range(3,9)}
    for entry, ts in entries:
        for n in lp_index:
            for g in grams(ts,n): lp_index[n][g].add(entry)
    matches=[]
    for n in lp_index:
        found=defaultdict(lambda:dict(entries=set(),pages=set(),files=set()))
        for row,txt,ts in documents:
            for g in grams(ts,n)&lp_index[n].keys():
                key=(row['book'],g)
                found[key]['entries'].update(lp_index[n][g])
                found[key]['pages'].add(row['display_page'])
                found[key]['files'].add(row['filename'])
        for (book,g),v in sorted(found.items()):
            matches.append(dict(book=book,words=n,phrase=' '.join(g),
                lp_entries=sorted(v['entries']),display_pages=sorted(v['pages']),
                screenshots=sorted(v['files'])))
    # Constructed checks verify matching and boundaries, not OCR accuracy.
    check1=('program','your','mind') in grams(tokens('He says: PROGRAM your mind.'),3)
    check2=tokens('informa-\ntional hygiene')==['informational','hygiene']
    check3=('alpha','beta','gamma') not in (grams(tokens('alpha beta'),3)|grams(tokens('gamma delta'),3))
    assert check1 and check2 and check3
    pages={(r['book'],r['display_page']) for r,_,_ in documents}
    anchors = {}
    for phrase in ['program your mind','program reality','command your own self',
                   'circumference','divinity','instar','totient']:
        needle=tuple(tokens(phrase))
        anchors[phrase]=[dict(book=b,display_page=p) for b,p in sorted({
            (row['book'],row['display_page']) for row,_,ts in documents
            if needle in grams(ts,len(needle))})]
    # UI and titles are retained in OCR; they are identified as a limitation.
    result=dict(method='Tesseract psm 6, lowercase ASCII word tokens, line-end hyphen rejoin; exact contiguous 3–8 token spans; no crossing screenshot or LP-entry boundaries.',
        limits=['OCR may miss or alter words; absent matches are not a full-text proof.',
                'Screenshots may begin/end mid-sentence; no cross-page matching.',
                'All supplied same-page variants searched; results grouped by book, page and phrase.',
                'Visible reader UI and headers remain in OCR; assess any apparent match against source images.',
                'This compares supplied pages, not full novels; no statistical authorship test.',
                'LP uses the retained community translation, including its spelling choices.'],
        tesseract_version=subprocess.run(['tesseract','--version'],capture_output=True,text=True,check=True).stdout.splitlines()[0],
        screenshots=len(documents),distinct_pages=len(pages),lp_entries=len(entries),lp_tokens=sum(len(t) for _,t in entries),
        translation_sha256=hashlib.sha256(translation.encode()).hexdigest(),
        controls=dict(inserted_phrase=check1,line_hyphen=check2,entry_boundary=check3),
        documents=[dict(**row,ocr_sha256=hashlib.sha256(txt.encode()).hexdigest(),ocr_tokens=len(ts)) for row,txt,ts in documents],
        unique_matching_phrases_by_length={str(n):sum(m['words']==n for m in matches) for n in lp_index},
        supplemental_lp_anchor_queries=anchors,
        matches=matches)
    (ROOT/'screenshot_phrase_results.json').write_text(json.dumps(result,indent=2,ensure_ascii=False)+'\n')
    print(json.dumps({k:v for k,v in result.items() if k not in ['documents','limits']},indent=2))

if __name__=='__main__':main()
