#!/usr/bin/env python3
"""Python3 stdlib: --release URL-or-file [--inputs CSV-file-or-URL].
Independent raw-feature/flag/count/rate verification. Does not verify bootstrap
intervals, the original event selection or outcome paths; use the original
verify-breakout-study.py for the latter. Never treat this as an execution model.
"""
import argparse,csv,hashlib,io,json,math,urllib.request
from pathlib import Path

def read(p):
    return urllib.request.urlopen(p,timeout=90).read() if p.startswith(('http://','https://')) else Path(p).read_bytes()

def smooth(v,n,alpha=None,start=0):
    out=[None]*len(v)
    if len(v)<start+n: return out
    out[start+n-1]=sum(v[start:start+n])/n
    for j in range(start+n,len(v)):
        out[j]=(alpha or 1/n)*v[j]+(1-(alpha or 1/n))*out[j-1]
    return out

def near(a,b):
    assert (a is None and b is None) or (a is not None and b is not None and math.isclose(a,b,rel_tol=1e-9,abs_tol=1e-9)),(a,b)

def main():
    p=argparse.ArgumentParser();p.add_argument('--release',required=True);p.add_argument('--inputs');a=p.parse_args()
    d=json.loads(read(a.release));raw=read(a.inputs or 'https://coinnudge.site'+d['inputs_url'])
    assert hashlib.sha256(raw).hexdigest()==d['input_sha256']
    groups={}
    for r in csv.DictReader(io.StringIO(raw.decode())):
        groups.setdefault(r['symbol'],[]).append({k:float(v) if v else None for k,v in r.items() if k not in ('symbol','source_sha256')})
    checked=0
    for symbol,bars in groups.items():
        bars.sort(key=lambda r:r['opened']);c=[r['c'] for r in bars];n=len(c)
        em={v:smooth(c,v,2/(v+1)) for v in (9,12,21,26,50)}
        line=[em[12][i]-em[26][i] if i>=25 else None for i in range(n)]
        sig=[None]*25+smooth(line[25:],9,2/10)
        hist=[line[i]-sig[i] if sig[i] is not None else None for i in range(n)]
        delta=[0]+[c[i]-c[i-1] for i in range(1,n)]
        gain=smooth([max(x,0) for x in delta],14,start=1);loss=smooth([max(-x,0) for x in delta],14,start=1)
        tr=[];pd=[0];md=[0]
        for i,r in enumerate(bars):
            tr.append(max(r['h']-r['l'],abs(r['h']-c[i-1]),abs(r['l']-c[i-1])) if i else r['h']-r['l'])
            if i:
                up=r['h']-bars[i-1]['h'];down=bars[i-1]['l']-r['l'];pd.append(up if up>down and up>0 else 0);md.append(down if down>up and down>0 else 0)
        trs,ps,ms=[smooth(v,14,start=1) for v in (tr,pd,md)]
        plus=[100*ps[i]/trs[i] if trs[i] else 0 for i in range(n)];minus=[100*ms[i]/trs[i] if trs[i] else 0 for i in range(n)]
        dx=[100*abs(x-y)/(x+y) if x+y else 0 for x,y in zip(plus,minus)];adx=smooth(dx,14,start=14)
        idx={r['opened']+3600:i for i,r in enumerate(bars)}
        def bands(j):
            v=c[j-19:j+1];mean=sum(v)/20;sd=(sum((x-mean)**2 for x in v)/20)**.5
            return mean+2*sd,4*sd/mean
        for r in d['technical_observations']:
            if r['symbol']!=symbol: continue
            j=idx[r['feature_time']];i=idx[r['event_time']];assert r['feature_time']==r['decision_time']
            rs=50 if gain[j]==loss[j]==0 else 100 if loss[j]==0 else 100-100/(1+gain[j]/loss[j])
            upper,width=bands(j);_,prior=bands(j-1)
            four=bars[i+1:j+1];q=sum(x['qv'] for x in four)
            valid=all(x['taker_buy_valid']==1 and x['taker_buy_qv'] is not None and 0<=x['taker_buy_qv']<=x['qv'] for x in four) and q>0
            buy=sum(x['taker_buy_qv'] for x in four) if valid else None
            session=[x for x in bars[max(0,j-23):j+1] if x['opened']//86400==bars[j]['opened']//86400]
            whole=session[0]['opened']%86400==0 and len(session)==int(bars[j]['opened']%86400/3600)+1 and all(x['v'] is not None and x['v']>0 and x['qv']>=0 for x in session)
            base=sum(x['v'] for x in session) if whole else None;quote=sum(x['qv'] for x in session) if whole else None;vw=quote/base if base else None
            distance=(c[j]-r['reference_level'])/r['atr_prior'];b=bars[i];wick=(b['h']-max(b['o'],b['c']))/(b['h']-b['l']) if b['h']>b['l'] else None
            values=dict(rsi=rs,macd_histogram=hist[j],macd_histogram_prior3=hist[j-3],bb_upper=upper,bb_width=width,bb_width_prior=prior,adx=adx[j],adx_prior3=adx[j-3],plus_di=plus[j],minus_di=minus[j],ema9=em[9][j],ema21=em[21][j],ema50=em[50][j],four_hour_quote=q,four_hour_buy_quote=buy,vwap_base=base,vwap_quote=quote,vwap=vw,distance_atr=distance,trigger_upper_wick_fraction=wick)
            for k,v in values.items(): near(v,r[k])
            flags=dict(rsi70=rs>=70,macd_growth=hist[j]>0 and hist[j]>hist[j-3],bb_expansion=width>prior and c[j]>upper,persistent_volume=q/4>=2*r['prior_quote_mean'],distance1=distance>=1,adx25=adx[j]>=25 and adx[j]>adx[j-3] and plus[j]>minus[j],ema_stack=c[j]>em[9][j]>em[21][j]>em[50][j],above_vwap=c[j]>vw if vw else None,buy55=buy/q>=.55 if valid else None,upper_wick=wick>=.4 if wick is not None else None)
            warm=j>=199 and bars[j]['opened']-bars[j-199]['opened']==199*3600
            assert warm==r['feature_warmup_valid']
            if not warm:
                for k in ('rsi70','macd_growth','bb_expansion','adx25','ema_stack'):flags[k]=None
            for k,v in flags.items(): assert r[k] is v,(r['observation_id'],k,r[k],v)
            checked+=1
    for section in ('technical_comparisons','technical_distance_strata'):
        for r in d[section]:
            pool=[x for x in d['technical_observations'] if x['symbol']==r['symbol']]
            if section=='technical_comparisons':pool=[x for x in pool if r['calendar_start']<=x['event_time'] and x['event_time']+76*3600<=r['calendar_end']]
            else:
                lo,hi=map(float,r['distance_bin'][1:-1].split(','));pool=[x for x in pool if lo<=x['distance_atr']<hi]
            for prefix,flag in (('yes',True),('no',False)):
                group=[x for x in pool if x[r['method']] is flag];fail=sum(x['failure_24h'] for x in group)
                assert len(group)==r[prefix+'_n'] and fail==r[prefix+'_failures']
                near(fail/len(group)*100 if group else None,r[prefix+'_failure_pct'])
            assert r['unavailable_n']==sum(x[r['method']] is None for x in pool)
    print(f'PASS: input hash, {checked} feature rows, flags, all comparison and distance-stratum counts/rates. Bootstrap intervals not checked by this script.')

if __name__=='__main__':main()
