#!/usr/bin/env python3
"""Build fair v3 journal preview from v3 reviews.

Includes:
- confirmed TP-first as Win
- shadow SL-first as Loss (valid entry touched)
Excludes:
- shadow no-touch observations
- rejected
- needs_more_context
Does not overwrite production journal/backtest-data.
"""
from __future__ import annotations
import json, statistics
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
REV=ROOT/'reviews/reviews_v3_htf_pdarray.json'
OUT=ROOT/'reviews/fair_journal_preview_v3_htf_pdarray.json'
SUMMARY=ROOT/'reports/fair_v3_stats_summary.json'
def parse_dt(s):
    dt=datetime.fromisoformat(str(s).replace('Z','+00:00'))
    if dt.tzinfo is None: dt=dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)
def iso(dt): return dt.replace(microsecond=0).isoformat().replace('+00:00','Z')
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 is_valid_loss(r):
    # v3 reviewer uses shadow for both no-touch and SL-first. Only SL-first includes entry_touch_time/outcome_time and r=-1.
    return r.get('decision')=='shadow' and r.get('r') == -1.0 and r.get('entry_touch_time') and r.get('outcome_time')
def is_valid_win(r): return r.get('decision')=='confirmed' and r.get('outcome')=='Win' and r.get('r') is not None
def canonical(r,idx):
    dt=parse_dt(r['timestamp']); outcome='Win' if is_valid_win(r) else 'Loss'
    planned=float(r.get('planned_rr') or abs(float(r.get('r') or 0)) or 2.0)
    realized=planned if outcome=='Win' else -1.0
    cand=r.get('candidate',{}) if isinstance(r.get('candidate'),dict) else {}
    fib_level=cand.get('levels',{}).get('fib_entry_level','n/a')
    # Candidate IDs in the v3 scanner can repeat when the same timestamp has
    # separate 50% and 62% entries. Make the source key entry-specific so the
    # preview can be audited/deduped unambiguously.
    source_key=f"{r['candidate_id']}|entry={r.get('entry')}|fib={fib_level}"
    return {'id':f"fair_v3_{dt.strftime('%Y%m%dT%H%M%SZ')}_{r.get('direction','na').lower()}_{idx:05d}",'date':dt.date().isoformat(),'timestamp':iso(dt),'symbol':'XAUUSD','strategy':r['strategy'],'direction':r.get('direction'),'session':sess(dt),'narrative':'V3 fair preview: HTF narrative + M15/M5 entry + PD-array; valid wins and valid losses included.','htf_context':r.get('htf_context') or cand.get('htf_narrative',{}).get('note','HTF context from v3 scanner'),'ltf_trigger':r.get('m15_m5_execution') or cand.get('notes'),'entry':r.get('entry'),'stop':r.get('stop'),'target':r.get('target'),'planned_rr':planned,'fib_entry_level':fib_level,'fib':'50%=2R only with strong PD Array; 62%=3R' if r['strategy']=='Aryy HTF Narrative Fib 50/62' else 'strategy-specific','pd':r['strategy'],'outcome':outcome,'r':realized,'confidence':'V3 fair preview, not production journal yet','notes':r.get('reason'),'source_candidate_id':source_key,'source_review':'hybrid/reviews/reviews_v3_htf_pdarray.json','is_confirmed_trade':True}
def maxdd(trades,risk=100,start=10000):
    eq=start; peak=eq; md=0; when=None
    for t in sorted(trades,key=lambda x:x['timestamp']):
        eq += float(t['r'])*risk; peak=max(peak,eq); dd=peak-eq
        if dd>md: md=dd; when=t['timestamp']
    return md/risk, md, when, eq
def stats(arr):
    c=Counter(t['outcome'] for t in arr); rs=[float(t['r']) for t in arr]; dd_r,dd,when,eq=maxdd(arr)
    return {'trades':len(arr),'wins':c.get('Win',0),'losses':c.get('Loss',0),'winrate':round(c.get('Win',0)/len(arr)*100,2) if arr else 0,'total_R':round(sum(rs),2) if rs else 0,'avg_R':round(statistics.mean(rs),3) if rs else 0,'maxDD_R':round(dd_r,2),'maxDD_usd':round(dd,2),'maxDD_when':when,'final_equity_100risk':round(eq,2)}
def main():
    reviews=json.loads(REV.read_text())['reviews']
    eligible=[r for r in reviews if is_valid_win(r) or is_valid_loss(r)]
    trades=[canonical(r,i+1) for i,r in enumerate(sorted(eligible,key=lambda x:x['timestamp']))]
    OUT.write_text(json.dumps({'metadata':{'purpose':'fair_v3_journal_preview','generated_at':iso(datetime.now(timezone.utc)),'source':'reviews_v3_htf_pdarray.json','trade_count':len(trades),'isFinalBacktest':False,'notes':'Preview only. Includes valid losses. Does not overwrite production journal.'},'trades':trades},indent=2,ensure_ascii=False)+'\n')
    by=defaultdict(list)
    for t in trades: by[t['strategy']].append(t)
    summary={'generated_at':iso(datetime.now(timezone.utc)),'overall':stats(trades),'strategy_stats':{k:stats(v) for k,v in sorted(by.items())},'fib_entry_counts':dict(Counter(t.get('fib_entry_level') for t in trades if t['strategy']=='Aryy HTF Narrative Fib 50/62')),'excluded_counts':dict(Counter(r.get('decision') for r in reviews if r not in eligible))}
    SUMMARY.write_text(json.dumps(summary,indent=2,ensure_ascii=False)+'\n')
    print(json.dumps(summary,indent=2))
if __name__=='__main__': main()
