#!/usr/bin/env python3
"""
patch_v3_monday.py
==================
Two fixes, both addressing the date-range deal-query weakness that the
2026-06-01 root-cause work uncovered:

PATCH A - reconcile heal placement:
  The position-query fallback currently sits AFTER the `if len(pair_deals)
  < 2: continue` guard, so it never runs for single-deal positions (exactly
  the ones that need it). Move the fallback BEFORE the length guard.

PATCH B - snapshot grouping:
  update_account_snapshot groups deals from the date-range query, so it
  under-counts positions whose exit deal the date-range query missed
  (the JSON=8 vs MT5=7 mismatch). Add a position-query fallback so each
  position's deal set is complete before P&L is summed.

Matches short, dash-free anchors to avoid em-dash encoding mismatches.
Usage:  python patch_v3_monday.py
"""
import sys, shutil, ast
from pathlib import Path

candidates = [
    Path(r"C:\Users\paul_\OneDrive\fx_macro_intraday\src\execution\live_signal_monitor_v3.py"),
    Path(r"C:\Users\Administrator\OneDrive\fx_macro_intraday\src\execution\live_signal_monitor_v3.py"),
    Path(__file__).parent / "live_signal_monitor_v3.py",
]
target = next((p for p in candidates if p.exists()), None)
if target is None:
    print("ERROR: could not find live_signal_monitor_v3.py")
    sys.exit(1)
print(f"Target file: {target}")

backup = target.with_suffix(".py.backup_monday")
if not backup.exists():
    shutil.copy2(target, backup)
    print(f"Backup created: {backup}")
else:
    print(f"Backup already exists: {backup}")

content = target.read_text(encoding="utf-8")

# ============================================================
# PATCH A: move reconcile position-query fallback BEFORE len<2 guard
# ============================================================
# First, REMOVE the misplaced fallback block (the one that currently sits
# after mt5_net_pnl = sum(...)). We match its distinctive lines.
old_a_remove = '''                # Compute the authoritative net P&L from MT5 deals
                mt5_net_pnl = sum(d.profit + d.swap + d.commission
                                  for d in pair_deals)

                # If date-range scan only saw the entry deal (IC Markets
                # quirk, see 2026-06-01 root cause), try the position query
                # which reliably returns the exit deal.
                if len([d for d in pair_deals if getattr(d, "entry", None) == 1]) == 0:
                    pos_specific = mt5.history_deals_get(position=int(pos_id))
                    if pos_specific and len(pos_specific) > len(pair_deals):
                        mt5_net_pnl = sum(d.profit + d.swap + d.commission
                                          for d in pos_specific)
                        pair_deals = sorted(pos_specific, key=lambda d: d.time)

                if int(pos_id) in existing_tickets:'''

new_a_clean = '''                # Compute the authoritative net P&L from MT5 deals
                mt5_net_pnl = sum(d.profit + d.swap + d.commission
                                  for d in pair_deals)

                if int(pos_id) in existing_tickets:'''

# Then, INSERT the fallback in the correct spot — before the len<2 guard.
old_a_guard = '''                # Fully closed position needs entry + exit (2+ deals)
                pair_deals = sorted(pair_deals, key=lambda d: d.time)
                if len(pair_deals) < 2:
                    continue  # still open or incomplete'''

new_a_guard = '''                # If the date-range scan saw fewer than 2 deals (e.g. only
                # the entry — a known IC Markets quirk, see 2026-06-01 root
                # cause), re-fetch via the position-specific query which
                # reliably returns both entry and exit deals. This must run
                # BEFORE the len<2 guard, otherwise single-deal positions get
                # skipped and never healed.
                if len(pair_deals) < 2:
                    pos_specific = mt5.history_deals_get(position=int(pos_id))
                    if pos_specific and len(pos_specific) > len(pair_deals):
                        pair_deals = list(pos_specific)
                # Fully closed position needs entry + exit (2+ deals)
                pair_deals = sorted(pair_deals, key=lambda d: d.time)
                if len(pair_deals) < 2:
                    continue  # still open or incomplete'''

if old_a_remove in content:
    content = content.replace(old_a_remove, new_a_clean)
    print("PATCH A step 1: removed misplaced fallback")
elif new_a_clean in content and old_a_guard not in content:
    print("PATCH A step 1: appears already cleaned")
else:
    print("WARNING: PATCH A step 1 anchor not found - fallback may differ.")

if old_a_guard in content:
    content = content.replace(old_a_guard, new_a_guard)
    print("PATCH A step 2: inserted fallback before len<2 guard")
elif new_a_guard in content:
    print("PATCH A step 2: already applied")
else:
    print("ERROR: PATCH A step 2 anchor (len<2 guard) not found. Aborting.")
    sys.exit(2)

# ============================================================
# PATCH B: snapshot grouping position-query fallback
# ============================================================
# In update_account_snapshot, after building by_position from date-range
# deals, augment any incomplete position with a position-specific query.
# Anchor: the line that sets mt5_pair_pnl right after the snapshot grouping.
old_b = '''            by_position.setdefault((d.symbol, d.position_id), []).append(d)
        mt5_pair_pnl    = {"EURUSD": 0.0, "USDJPY": 0.0}'''

new_b = '''            by_position.setdefault((d.symbol, d.position_id), []).append(d)
        # Augment incomplete positions (date-range query sometimes omits the
        # exit deal on IC Markets demo — 2026-06-01 root cause). For any
        # position with <2 deals, re-fetch via the position-specific query.
        for (_sym, _pid), _dl in list(by_position.items()):
            if len(_dl) < 2:
                _ps = mt5.history_deals_get(position=_pid)
                if _ps and len(_ps) > len(_dl):
                    by_position[(_sym, _pid)] = [d for d in _ps
                                                 if d.symbol == _sym]
        mt5_pair_pnl    = {"EURUSD": 0.0, "USDJPY": 0.0}'''

if old_b in content:
    content = content.replace(old_b, new_b)
    print("PATCH B applied: snapshot grouping uses position-query fallback")
elif new_b in content:
    print("PATCH B skipped: already applied")
else:
    print("WARNING: PATCH B anchor not found - snapshot not patched.")
    print("  (Patches A still apply.)")

# ============================================================
# Verify + write
# ============================================================
try:
    ast.parse(content)
    print("Syntax check: OK")
except SyntaxError as e:
    print(f"ERROR: syntax error after patch: {e}. Aborting, no changes written.")
    sys.exit(3)

target.write_text(content, encoding="utf-8")
print(f"\nSUCCESS: patched {target}")
print("\nNext steps:")
print("  1. Restart v3 (Ctrl+C, up arrow, Enter).")
print("  2. Reconcile should now show no stale-zero entries (all healthy).")
print("  3. Snapshot should show matching JSON/MT5 counts (no 'using JSON' warning).")
