#!/usr/bin/env python3
"""Ribbon Garden: real WSRF continuation regression, candidate selection, held-out play."""
import sys,json,hashlib,inspect,platform,subprocess,importlib.metadata
from pathlib import Path
import numpy as np
import pytoolz as pt
ROOT=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(ROOT/'tools/academy'))
from train import fit,export,arrays,save
CACHE=ROOT/'tools/ribbon/.cache';CACHE.mkdir(exist_ok=True)
ID='ribbon-garden';OUT=ROOT/'games'/ID
def samples(split,n,**kw):
    seed_split=kw.pop('seed_split',split);ix=pt.indexrng.Index('ribbon-garden-v3',backend='blake2b')
    seeds=[f'{seed_split}-{i}-{int(w)}' for i,w in enumerate(ix.words(seed_split,n))]
    cfg=CACHE/(split+'-request.json');output=CACHE/(split+'.json');save(cfg,dict(seeds=seeds,output=str(output),**kw))
    print(split,subprocess.check_output(['node',str(ROOT/'tools/ribbon/data.cjs'),str(cfg)],text=True).strip(),flush=True)
    return json.loads(output.read_text())
def score(r):return(r['wins'],r['mean_score'],-r['mean_turns'])
def main():
    features=json.loads(subprocess.check_output(['node','-e',"console.log(JSON.stringify(require('./games/ribbon-garden/model.js').featuresNames))"],cwd=ROOT,text=True))
    rows=samples('train',256,collect=True,explore=True)['rows'];rows+=samples('recovery-parent',128,collect=True,guided=True,model=str(OUT/'training/parent-forest.json'))['rows'];early_count=len(rows)
    early=fit(rows,12,12);ep=CACHE/'early-forest.json';save(ep,export(early,ID,features))
    ev=samples('validation-early',32,seed_split='validation',model=str(ep))['summary']
    rows+=samples('recovery',48,collect=True,guided=True,model=str(ep))['rows']
    final=fit(rows,32,18);fp=CACHE/'final-forest.json';save(fp,export(final,ID,features))
    fv=samples('validation-final',32,seed_split='validation',model=str(fp))['summary']
    chosen,mp,label=(final,fp,'final') if score(fv)>=score(ev) else (early,ep,'early')
    test=samples('test',64,model=str(mp),collect=True);guided=samples('guided',32,model=str(mp),guided=True)
    # collect must not change evaluation priorities; use explicitly recorded test conditions.
    teacher=samples('test-reference',64,seed_split='test');random=samples('test-random',64,seed_split='test',random=True);et=samples('test-early',64,seed_split='test',model=str(ep),collect=True)
    hx,hz,hy=arrays(test['rows']);pred=chosen.predict(hx,zones=hz)
    fx=list(hx[:3000]);fz=list(hz[:3000])
    for zone,trees in chosen.zone_trees_.items():
        refs=hx[hz==zone]
        for tree in trees:
            for i,(feature,t) in enumerate(zip(tree.tree_.feature,tree.tree_.threshold)):
                if feature<0:continue
                center=np.float32(t)
                for value in [np.nextafter(center,np.float32(-np.inf)),center,np.nextafter(center,np.float32(np.inf))]:
                    x=refs[i%len(refs)].copy();x[feature]=value;fx.append(x);fz.append(zone)
    fy=chosen.predict(np.array(fx),zones=np.array(fz));fixtures=[dict(x=x.tolist(),zone=str(z),y=float(y)) for x,z,y in zip(fx,fz,fy)]
    fixture=CACHE/'validation.json';save(fixture,fixtures)
    parity=json.loads(subprocess.check_output(['node',str(ROOT/'tools/academy/validate.cjs'),str(mp),str(fixture)],text=True))
    (OUT/'training').mkdir(exist_ok=True);(OUT/'forest.json').write_bytes(mp.read_bytes());(OUT/'early-forest.json').write_bytes(ep.read_bytes());(OUT/'training/validation.json').write_bytes(fixture.read_bytes())
    paths=['games/ribbon-garden/model.js','tools/ribbon/data.cjs','tools/ribbon/train.py','shared/academy-core.js','shared/academy-forest.js','shared/pytoolz-lite.js','tools/academy/train.py','games/ribbon-garden/training/parent-forest.json']
    estimator=Path(inspect.getfile(pt.wsrf.WSRFRegressor)).resolve();clean=lambda x:{k:v for k,v in x.items() if k!='rows'}
    report=dict(game=ID,library='pytoolz.wsrf.WSRFRegressor',learning='Visible routing and escape score with visit-memory penalty plus a bounded six-step continuation adjustment.',namespace='ribbon-garden-v3',training_rows=len(rows) if label=='final' else early_count,candidate_training_rows=len(rows),early_rows=early_count,features=features,selected=label,parameters=chosen.get_params(),validation_early=ev,validation_final=fv,test=clean(test),guided_test=clean(guided),teacher_baseline=clean(teacher),random_baseline=clean(random),early_test=clean(et),held_out_action_value_mae=float(np.mean(np.abs(pred-hy))),held_out_action_value_r2=float(chosen.score(hx,hy,zones=hz)),export_validation=parity,trees=sum(len(t) for t in chosen.zone_trees_.values()),versions={**{p:importlib.metadata.version(p) for p in ['numpy','pandas','scikit-learn']},'python':platform.python_version(),'wsrf':pt.load('wsrf').__version__},wsrf_source=dict(path=str(estimator),sha256=hashlib.sha256(estimator.read_bytes()).hexdigest()),arcade_source_sha256={p:hashlib.sha256((ROOT/p).read_bytes()).hexdigest() for p in paths},forest_sha256=hashlib.sha256((OUT/'forest.json').read_bytes()).hexdigest())
    save(OUT/'training/report.json',report);save(OUT/'stats.json',dict(rows=report['training_rows'],trees=report['trees'],wins=test['summary']['wins'],days=64,parity=parity))
    print('SELECTED',label,'TEST',test['summary'],'GUIDED',guided['summary'],'PARITY',parity,flush=True)
if __name__=='__main__':main()
