#!/usr/bin/env python3
"""Deterministic v2 multi-strategy reviewer/promoter.

This reviews phase-2 candidates from scan_candidates_v2_multistrategy.py and
creates two outputs:
- confirmed trades suitable for hybrid journal/backtest-data append
- shadow observations suitable for hybrid shadow-trades append

This is still deterministic/local OHLCV review, not broker execution.
"""
from __future__ import annotations

import argparse, json, statistics
from collections import Counter
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any

ROOT=Path(__file__).resolve().parents[1]
DEFAULT_CAND=ROOT/'candidates/candidates_v2_multistrategy.json'
DEFAULT_REVIEWS=ROOT/'reviews/reviews_v2_multistrategy.json'
DEFAULT_CONFIRMED=ROOT/'reviews/confirmed_v2_multistrategy.json'
DEFAULT_SHADOW=ROOT/'reviews/shadow_v2_multistrategy.json'
DEFAULT_SUMMARY=ROOT/'reports/v2_multistrategy_review_summary.json'

def now_iso(): return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00','Z')
def parse_dt(v:Any)->datetime:
    if isinstance(v,(int,float)): return datetime.fromtimestamp(v if v<1e10 else v/1000,tz=timezone.utc)
    dt=datetime.fromisoformat(str(v).replace('Z','+00:00'))
    if dt.tzinfo is None: dt=dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)
def load_json(p:Path): return json.loads(p.read_text())
def rows(o):
    if isinstance(o,list): return o
    for k in ['bars','ohlcv','data','candles','values']:
        if isinstance(o,dict) and isinstance(o.get(k),list): return o[k]
    raise ValueError('missing rows')
def load_bars(tf):
    obj=load_json(ROOT/'data'/f'ohlcv_{tf}.json'); out=[]
    for b in rows(obj):
        t=parse_dt(b.get('timestamp',b.get('time',b.get('date'))))
        out.append((t,{k:float(b[k]) for k in ['open','high','low','close']}))
    return sorted(out,key=lambda x:x[0])
def session(ts):
    h=parse_dt(ts).hour
    if 0<=h<7: return 'Asia'
    if 7<=h<10: return 'London'
    if 12<=h<17: return 'NY AM'
    if 15<=h<17: return 'London Close'
    if 17<=h<21: return 'NY PM'
    return 'Other'
def window(bars,dt,before=24,after=96):
    import bisect
    times=[x[0] for x in bars]; i=bisect.bisect_right(times,dt)-1
    return bars[max(0,i-before+1):i+1], bars[i+1:min(len(bars),i+1+after)], i

def hit(direction,entry,stop,target,post):
    for dt,b in post:
        if direction=='Long': sl=b['low']<=stop; tp=b['high']>=target
        else: sl=b['high']>=stop; tp=b['low']<=target
        if sl and tp: return 'ambiguous_same_bar',dt
        if sl: return 'sl_first',dt
        if tp: return 'tp_first',dt
    return 'unresolved',None

def levels_entry(c):
    lv=c.get('levels') or {}; direction=c['direction']; fam=c['strategy_family']
    if fam=='fib_50_62':
        z1=float(lv['fib50']); z2=float(lv['fib62']); entry=(z1+z2)/2
        if direction=='Long': stop=float(lv['impulse_low']); risk=entry-stop; target=entry+risk*1.5
        else: stop=float(lv['impulse_high']); risk=stop-entry; target=entry-risk*1.5
        return entry,stop,target,1.5
    if fam=='sweep_mss_fvg':
        if direction=='Long': entry=(float(lv['fvg_low'])+float(lv['fvg_high']))/2; stop=float(lv['sweep_low']); risk=entry-stop; target=entry+risk*2
        else: entry=(float(lv['fvg_low'])+float(lv['fvg_high']))/2; stop=float(lv['sweep_high']); risk=stop-entry; target=entry-risk*2
        return entry,stop,target,2.0
    if fam=='order_block_reclaim':
        entry=(float(lv['ob_low'])+float(lv['ob_high']))/2
        if direction=='Long': stop=float(lv['ob_low']); risk=entry-stop; target=entry+risk*2
        else: stop=float(lv['ob_high']); risk=stop-entry; target=entry-risk*2
        return entry,stop,target,2.0
    if fam=='breaker_continuation':
        entry=(float(lv['breaker_low'])+float(lv['breaker_high']))/2
        if direction=='Long': stop=float(lv['breaker_low']); risk=entry-stop; target=entry+risk*2
        else: stop=float(lv['breaker_high']); risk=stop-entry; target=entry-risk*2
        return entry,stop,target,2.0
    raise ValueError(f'unknown family {fam}')

def review(c,m5,m15,m30,h1):
    dt=parse_dt(c['timestamp']); direction=c['direction']; fam=c['strategy_family']
    try: entry,stop,target,rr=levels_entry(c)
    except Exception as e:
        return {'candidate_id':c['id'],'decision':'rejected','reason':f'level parse failed: {e}','candidate':c}
    if direction=='Long' and not (stop<entry<target):
        return {'candidate_id':c['id'],'decision':'rejected','reason':'invalid long geometry','candidate':c,'entry':entry,'stop':stop,'target':target}
    if direction=='Short' and not (target<entry<stop):
        return {'candidate_id':c['id'],'decision':'rejected','reason':'invalid short geometry','candidate':c,'entry':entry,'stop':stop,'target':target}
    pre5,post5,_=window(m5,dt,30,240); pre15,post15,_=window(m15,dt,20,64); pre30,_,_=window(m30,dt,20,24); preh1,_,_=window(h1,dt,20,8)
    # entry touch after candidate
    touch=False; touch_dt=None
    for t,b in post5:
        if b['low']<=entry<=b['high']:
            touch=True; touch_dt=t; break
    if not touch:
        return {'candidate_id':c['id'],'decision':'shadow','reason':'candidate formed but proposed entry not touched in M5 follow-through window','candidate':c,'entry':entry,'stop':stop,'target':target,'r':None}
    post_after=[x for x in post5 if x[0]>=touch_dt]
    outcome,out_dt=hit(direction,entry,stop,target,post_after)
    if outcome=='tp_first': dec='confirmed'; realized=rr
    elif outcome=='sl_first': dec='shadow'; realized=-1.0
    elif outcome=='ambiguous_same_bar': dec='needs_more_context'; realized=None
    else: dec='needs_more_context'; realized=None
    # HTF summaries only
    pre15hi=max(x[1]['high'] for x in pre15) if pre15 else None; pre15lo=min(x[1]['low'] for x in pre15) if pre15 else None
    reason=f"{c['strategy']} {direction}: entry touched {touch_dt.isoformat().replace('+00:00','Z')}; outcome={outcome}" if touch_dt else 'no touch'
    return {'candidate_id':c['id'],'decision':dec,'reason':reason,'timestamp':c['timestamp'],'direction':direction,'strategy':c['strategy'],'strategy_family':fam,'session':c['session'],'entry':round(entry,3),'stop':round(stop,3),'target':round(target,3),'r':realized,'entry_touch_time':touch_dt.isoformat().replace('+00:00','Z') if touch_dt else None,'outcome_time':out_dt.isoformat().replace('+00:00','Z') if out_dt else None,'htf_context':f"H1/M30 context mechanically checked around {c['timestamp']}.",'m30_structure':f"M30 recent range high/low checked; strategy={c['strategy']}.",'m15_m5_execution':f"M15/M5 candidate levels: entry={round(entry,3)}, stop={round(stop,3)}, target={round(target,3)}. Pre-M15 range high={round(pre15hi,3) if pre15hi else None}, low={round(pre15lo,3) if pre15lo else None}.",'candidate':c}

def trade_from_review(r,idx):
    dt=parse_dt(r['timestamp']); direction=r['direction']
    return {'id':f"hybrid_v2_{dt.strftime('%Y%m%dT%H%M%SZ')}_{direction.lower()}_{idx:04d}",'date':dt.date().isoformat(),'timestamp':dt.isoformat().replace('+00:00','Z'),'symbol':'XAUUSD','strategy':r['strategy'],'direction':direction,'session':session(dt.isoformat()),'narrative':f"Phase-2 multi-strategy confirmed audit: {r['strategy']}",'htf_context':r['htf_context'],'ltf_trigger':r['m15_m5_execution'],'entry':r['entry'],'stop':r['stop'],'target':r['target'],'fib':'strategy-specific v2 audit','pd':r['strategy'],'outcome':'Win' if (r.get('r') or 0)>0 else 'Loss','r':r['r'],'confidence':'V2 deterministic confirmed; still analysis-only backtest','notes':r['reason'],'source_candidate_id':r['candidate_id'],'source_review':'hybrid/reviews/reviews_v2_multistrategy.json','is_confirmed_trade':True}

def shadow_from_review(r):
    dt=parse_dt(r.get('timestamp') or r['candidate'].get('timestamp'))
    return {'id':f"shadow_{r['candidate_id']}",'date':dt.date().isoformat(),'timestamp':dt.isoformat().replace('+00:00','Z'),'symbol':'XAUUSD','type':'Shadow observation','strategy':r.get('strategy') or r['candidate'].get('strategy'),'direction':r.get('direction') or r['candidate'].get('direction'),'entry':r.get('entry'),'stop':r.get('stop'),'target':r.get('target'),'r':r.get('r'),'note':r.get('reason','V2 shadow observation; not confirmed trade.'),'source_candidate_id':r['candidate_id'],'source_result_file':'hybrid/reviews/reviews_v2_multistrategy.json','is_confirmed_trade':False}

def main():
    ap=argparse.ArgumentParser(); ap.add_argument('--candidates',type=Path,default=DEFAULT_CAND); args=ap.parse_args()
    cand=load_json(args.candidates)['candidates']; m5=load_bars('M5'); m15=load_bars('M15'); m30=load_bars('M30'); h1=load_bars('H1')
    reviews=[review(c,m5,m15,m30,h1) for c in cand]
    counts=Counter(r['decision'] for r in reviews); fam_counts=Counter((r.get('strategy') or r['candidate'].get('strategy'), r['decision']) for r in reviews)
    confirmed=[trade_from_review(r,i+1) for i,r in enumerate([x for x in reviews if x['decision']=='confirmed'])]
    shadows=[shadow_from_review(r) for r in reviews if r['decision']=='shadow']
    ts=now_iso()
    DEFAULT_REVIEWS.write_text(json.dumps({'metadata':{'purpose':'v2_multistrategy_reviews','generated_at':ts,'source':str(args.candidates),'review_count':len(reviews),'breakdown':dict(counts),'isFinalBacktest':False},'reviews':reviews},indent=2,ensure_ascii=False)+'\n')
    DEFAULT_CONFIRMED.write_text(json.dumps({'metadata':{'purpose':'v2_confirmed_trade_preview','generated_at':ts,'count':len(confirmed),'isFinalBacktest':False},'trades':confirmed},indent=2,ensure_ascii=False)+'\n')
    DEFAULT_SHADOW.write_text(json.dumps({'metadata':{'purpose':'v2_shadow_preview','generated_at':ts,'count':len(shadows),'isFinalBacktest':False},'shadowTrades':shadows},indent=2,ensure_ascii=False)+'\n')
    summary={'generated_at':ts,'review_count':len(reviews),'breakdown':dict(counts),'confirmed_count':len(confirmed),'shadow_count':len(shadows),'strategy_decision_counts':{f'{k[0]}|{k[1]}':v for k,v in fam_counts.items()}}
    DEFAULT_SUMMARY.write_text(json.dumps(summary,indent=2,ensure_ascii=False)+'\n')
    print('review_count',len(reviews)); print('breakdown',dict(counts)); print('confirmed',len(confirmed),'shadow',len(shadows)); print('summary',DEFAULT_SUMMARY)
if __name__=='__main__': main()
