"""Session 16. Fixed direction tests motivated by the 1033/3301 motif.
No keys fitted or expanded after outputs. Run with Python + NumPy.
"""
import hashlib, json, re
from word_core import *

OPS=['forward','reverse_words','reverse_lines','reverse_after_each_marker','alternate_at_markers']
MAPS={'direct':(1,0),'atbash':(-1,28),'shifted_atbash':(-1,2)}

def transform(chunks, op):
    if op=='forward':return chunks.copy()
    if op=='reverse_words':
        source='%'.join(chunks)
        v=np.concatenate([np.array(w['values'][::-1]) for w in tokenize(source)])
        result=replace_values(source,v)
        # Split on original marker boundaries, not on every page delimiter.
        out=[];at=0
        for c in chunks:
            out.append(result[at:at+len(c)]);at+=len(c)+1
        return out
    if op=='reverse_lines':
        out=[]
        for c in chunks:
            parts=re.split(r'([/%])',c)
            out.append(''.join(p if i%2 else p[::-1] for i,p in enumerate(parts)))
        return out
    if op=='reverse_after_each_marker':
        return [c if i==0 else c[::-1] for i,c in enumerate(chunks)]
    if op=='alternate_at_markers':
        return [c if i%2==0 else c[::-1] for i,c in enumerate(chunks)]
    raise ValueError(op)

def apply_map(chunks,name):
    a,b=MAPS[name]
    return [replace_values(c,(a*vals(c)+b)%29) for c in chunks]

def main():
    # Fixture: word one crosses a physical line, word two crosses a page.
    fixture=['ᚠ/ᚢ-ᚦ','ᚩ-ᚱ']
    assert transform(fixture,'reverse_words')==['ᚢ/ᚠ-ᚩ','ᚦ-ᚱ']
    assert transform(['ᚠᚢ-ᚦ/ᚩᚱ'],'reverse_lines')==['ᚦ-ᚢᚠ/ᚱᚩ']
    assert transform(['ᚠᚢ','ᚦᚩ','ᚱᚳ','ᚷᚹ','ᚻᚾ'],'alternate_at_markers')==['ᚠᚢ','ᚩᚦ','ᚱᚳ','ᚹᚷ','ᚻᚾ']
    # Independent historical substitution control: exact known warning runes.
    warning=render(pages[0],(28-vals(pages[0]))%29)
    assert warning.startswith('A WARNING BELIEUE NOTHING FROM THIS BOOK')
    # Constructed recovery controls use held-out LP2p57 in five marker blocks.
    plain=[lp[57]]*5;controls=[]
    for op in OPS:
        for mapping in MAPS:
            cipher=transform(apply_map(plain,mapping),op)
            recovered=apply_map(transform(cipher,op),mapping)
            assert recovered==plain,(op,mapping)
            controls.append(dict(order=op,mapping=mapping,exact=True,runes=475))
    chunks=[lp[33].split('$')[1],lp[34],'%'.join(lp[35:38]),lp[38],lp[39].split('$')[0]]
    source='%'.join(chunks);canonical='%'.join(lp[:56]).split('$')[7]
    assert np.array_equal(vals(source),vals(canonical))
    lengths=[len(vals(x)) for x in chunks];cuts=np.cumsum(lengths)[:-1]
    cross=[]
    for cut in cuts:
        cross.append([dict(runes=w['runes'],offset=w['rune_offset']) for w in tokenize(source)
                      if w['rune_offset']<cut<w['rune_offset']+len(w['values'])])
    out=dict(marker_pages=[33,35,38,39],marker_placements=['bottom','top','top','top'],
        segment_runes=lengths,marker_offsets=cuts.tolist(),words_crossing_markers=cross,
        runes=len(vals(source)),fixture_checks=3,controls=controls,warning_control=warning,
        input_sha256=hashlib.sha256((ROOT/'liber-primus__transcription--master.txt').read_bytes()).hexdigest(),
        methods=MAPS,results=[],limits=[
            'All direction rules exploratory: the image audit does not select a unique reversal.',
            'No page reordering: forward list labels 1..5 are evidence against it.',
            'Character reversal carries separators and line/page markers with the text; glyph shapes are not mirrored.',
            'Word reversal preserves whole words across page/line wraps; line reversal uses physical transcription lines.',
            'Marker modes retain the pre-marker fragment; other blocks follow actual four marker positions.',
            'Only direct reading and two known fixed LP1 substitutions; no arithmetic streams or extra layers.',
            'Language scores are descriptive, use the whole output, and are not significance tests or held-out validation.',
            'Preserved Arabic list digits do not enter rune arithmetic; their positions move with reversed text.',
            'Synthetic recovery does not establish that the real manuscript uses these transformations.'
        ])
    for op in OPS:
        for mapping in MAPS:
            text='%'.join(apply_map(transform(chunks,op),mapping))
            v=vals(text);words=tokenize(text)
            score=float(FULL_MODEL.score(v[None,:],[len(w['values']) for w in words])[0])
            out['results'].append(dict(order=op,mapping=mapping,score=score,text=render(text,v),runes=text))
    out['candidate_count']=len(out['results'])
    (ROOT/'reversal_results.json').write_text(json.dumps(out,indent=2,ensure_ascii=False))
    for r in out['results']:print(r['order'],r['mapping'],round(r['score'],4),r['text'][:130])
    print('Candidates',out['candidate_count'],'controls',len(controls),'all exact')

if __name__=='__main__':main()
