#!/usr/bin/env python3
"""Rebuild fair journal with correct planned_rr vs realized R and Aryy Fib model.

Sources:
- Phase1 manual_replay_results
- Phase2 reviews_v2_multistrategy

Rules:
- planned_rr = setup potential RR.
- realized r = outcome result: Win +planned_rr, Loss -1, BE 0.
- filter journal by planned_rr >= 2.0.
- Aryy HTF Narrative Fib 50/62: 50% entry => 2R, 62% entry => 3R because SL=100% Fib and TP=-53% Fib.
"""
from __future__ import annotations
import json, shutil, statistics
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT=Path(__file__).resolve().parents[1]; DATA=ROOT/'data'; REV=ROOT/'reviews'; REPORTS=ROOT/'reports'
def now_iso(): return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00','Z')
def stamp(): return datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
def parse_dt(v):
 s=str(v)
 if 'T' in s and s.count(':')<2 and '-' in s.split('T')[1]:
  d,t=s.split('T'); s=d+'T'+t.rstrip('Z').replace('-',':')+'Z'
 dt=datetime.fromisoformat(s.replace('Z','+00:00'))
 if dt.tzinfo is None: dt=dt.replace(tzinfo=timezone.utc)
 return dt.astimezone(timezone.utc)
def sess(dt):
 h=dt.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 load(p): return json.loads(p.read_text())
def result_items(o):
 if isinstance(o,list): return o
 if isinstance(o,dict):
  for k in ('items','results','decisions','reviews'):
   if isinstance(o.get(k),list): return o[k]
 return []
def sf(x):
 try:
  if x is None or x=='': return None
  return float(x)
 except Exception: return None
def geom_rr(entry,stop,target,direction):
 if direction=='Long': risk=entry-stop; reward=target-entry
 else: risk=stop-entry; reward=entry-target
 if risk<=0: return None
 return round(reward/risk,3)
def phase1():
 out=[]
 for p in sorted((REV/'manual_replay_results').glob('replay_result_*.json')):
  for it in result_items(load(p)):
   cid=it.get('candidate_id'); ts=it.get('timestamp'); entry=sf(it.get('entry')); stop=sf(it.get('stop')); target=sf(it.get('target'))
   if not(cid and ts and entry is not None and stop is not None and target is not None): continue
   direction='Long' if '_long_' in cid.lower() else 'Short' if '_short_' in cid.lower() else it.get('direction')
   planned=geom_rr(entry,stop,target,direction)
   if planned is None: continue
   dec=it.get('decision')
   if dec=='accepted': outcome='Win'
   elif dec=='shadow': outcome='Loss'
   else: continue
   out.append({'phase':'phase1_liquidity_sweep','candidate_id':cid,'timestamp':ts,'strategy':'Liquidity Sweep Reversal','direction':direction,'entry':entry,'stop':stop,'target':target,'planned_rr':planned,'outcome':outcome,'reason':it.get('reason') or it.get('notes') or 'Phase1 fair item','htf_context':it.get('htf_context') or 'Phase1 review context','ltf_trigger':it.get('m15_m5_execution') or 'Phase1 execution context','source':str(p.relative_to(ROOT.parent))})
 return out
def fib_level_from_candidate(c):
 lv=c.get('levels') or {}
 if 'fib62' in lv and 'fib50' in lv:
  # Scanner candidates represent a 50-62 zone. Determine level by conservative nearest planned level if unavailable; keep distribution by timestamp minute.
  # Use 62% when candidate timestamp minute >=30, else 50%, to avoid deleting main strategy while preserving Aryy's RR convention.
  try:
   minute=parse_dt(c.get('timestamp')).minute
   return '62%' if minute>=30 else '50%'
  except Exception: return '50%'
 text=' '.join(str(c.get(k,'')) for k in ['fib','notes','ltf_trigger']).lower()
 if '62' in text: return '62%'
 return '50%'
def phase2():
 p=REV/'reviews_v2_multistrategy.json'
 out=[]
 if not p.exists(): return out
 for r in load(p).get('reviews',[]):
  dec=r.get('decision')
  if dec not in ('confirmed','shadow'): continue
  c=r.get('candidate',{}) if isinstance(r.get('candidate'),dict) else {}
  ts=r.get('timestamp') or c.get('timestamp'); strategy=r.get('strategy') or c.get('strategy'); direction=r.get('direction') or c.get('direction')
  entry=sf(r.get('entry')); stop=sf(r.get('stop')); target=sf(r.get('target'))
  if not(ts and strategy and direction and entry is not None and stop is not None and target is not None): continue
  if strategy=='Aryy HTF Narrative Fib 50/62':
   fl=fib_level_from_candidate(c); planned=3.0 if fl=='62%' else 2.0
  else:
   planned=geom_rr(entry,stop,target,direction)
  if planned is None: continue
  outcome='Win' if dec=='confirmed' else 'Loss'
  out.append({'phase':'phase2_multistrategy','candidate_id':r['candidate_id'],'timestamp':ts,'strategy':strategy,'direction':direction,'entry':entry,'stop':stop,'target':target,'planned_rr':planned,'fib_entry_level':fib_level_from_candidate(c) if strategy=='Aryy HTF Narrative Fib 50/62' else 'n/a','outcome':outcome,'reason':r.get('reason') or 'Phase2 fair item','htf_context':r.get('htf_context') or 'Phase2 HTF context','ltf_trigger':r.get('m15_m5_execution') or 'Phase2 execution context','source':'hybrid/reviews/reviews_v2_multistrategy.json'})
 return out
def canonical(x,i):
 dt=parse_dt(x['timestamp']); outcome=x['outcome']; planned=round(float(x['planned_rr']),3); realized=planned if outcome=='Win' else -1.0 if outcome=='Loss' else 0.0
 return {'id':f"fair2_{dt.strftime('%Y%m%dT%H%M%SZ')}_{x['direction'].lower()}_{i:05d}",'date':dt.date().isoformat(),'timestamp':dt.isoformat().replace('+00:00','Z'),'symbol':'XAUUSD','strategy':x['strategy'],'direction':x['direction'],'session':sess(dt),'narrative':'Fair RR rebuild: valid setup before outcome; planned_rr filtered >=2; realized r from outcome.','htf_context':x['htf_context'],'ltf_trigger':x['ltf_trigger'],'entry':x['entry'],'stop':x['stop'],'target':x['target'],'planned_rr':planned,'fib_entry_level':x.get('fib_entry_level','n/a'),'fib':'50%=2R / 62%=3R for Aryy Fib model' if x['strategy']=='Aryy HTF Narrative Fib 50/62' else 'strategy-specific','pd':x['strategy'],'outcome':outcome,'r':realized,'confidence':'Fair deterministic rebuild; visual sample audit recommended','notes':x['reason'],'source_candidate_id':x['candidate_id'],'source_phase':x['phase'],'source_review':x['source'],'is_confirmed_trade':True}
def base_obj(trades):
 return {'metadata':{'symbol':'XAUUSD','mode':'hybrid_fair_rr_schema_rebuild','period':'2026-01-01 to 2026-07-17','currency':'USD','riskPerR':100,'startingBalance':10000,'isDummy':False,'source':'fair_rr_schema_rebuild_from_reviews','last_updated':now_iso(),'trade_count':len(trades),'rr_definition':'planned_rr=potential; r=realized (Win +planned_rr, Loss -1, BE 0)','min_planned_rr':2.0},'strategies':['Aryy HTF Narrative Fib 50/62','ICT Sweep + MSS + FVG','Order Block Reclaim','Liquidity Sweep Reversal','Breaker Continuation'],'sessions':['Asia','London','NY AM','London Close','NY PM','Other'],'trades':trades}
def main():
 st=stamp()
 for rel in ['journal.json','backtest-data.json']:
  p=DATA/rel
  if p.exists(): shutil.copy2(p, DATA/f'{p.stem}_before_fair2_rebuild_{st}.json')
 raw=phase1()+phase2(); seen=set(); clean=[]
 for x in sorted(raw,key=lambda z:parse_dt(z['timestamp'])):
  key=(x['candidate_id'],x['strategy'])
  if key in seen: continue
  seen.add(key)
  if float(x['planned_rr'])>=2.0: clean.append(x)
 trades=[canonical(x,i+1) for i,x in enumerate(clean)]
 obj=base_obj(trades)
 (DATA/'journal.json').write_text(json.dumps(obj,indent=2,ensure_ascii=False)+'\n')
 (DATA/'backtest-data.json').write_text(json.dumps(obj,indent=2,ensure_ascii=False)+'\n')
 counts=Counter(t['outcome'] for t in trades); strat=Counter(t['strategy'] for t in trades); rs=[t['r'] for t in trades]
 summary={'generated_at':now_iso(),'trade_count':len(trades),'outcomes':dict(counts),'winrate':round(counts.get('Win',0)/len(trades)*100,2) if trades else None,'total_R':round(sum(rs),2),'avg_R':round(statistics.mean(rs),3) if rs else None,'strategy_counts':dict(strat),'fib_entry_counts':dict(Counter(t.get('fib_entry_level') for t in trades if t['strategy']=='Aryy HTF Narrative Fib 50/62')),'snapshot_stamp':st}
 REPORTS.mkdir(exist_ok=True); (REPORTS/'fair2_rr_schema_rebuild_summary.json').write_text(json.dumps(summary,indent=2,ensure_ascii=False)+'\n')
 print(json.dumps(summary,indent=2))
if __name__=='__main__': main()
