"""Remove double-booked bets from bet_history.json and rebuild the bankrolls.

THE BUG. Dedupe in both logging paths consulted pending_bets.json only, which
protects a bet just while it is OPEN. A match that kicked off early and settled
between two runs of the job left pending, so the next run logged the same signal
again and booked its P&L twice. Found 2026-08-08 via the nightly email showing
Independiente Rivadavia v Estudiantes de Rio Cuarto twice (baseline, Away,
-1.67 each). Fixed at source in daily_signals._settled_keys and the matching
guard in live_monitor._log_pending_bets.

THE REPAIR. Keyed on (date, home, away, side, strategy): keep the FIRST row,
drop later identical ones. A rematch on a different date is a different key and
is untouched. Nothing is deleted outright -- dropped rows are written to a
sidecar file so the decision stays auditable.

Bankrolls are then replayed from scratch over the deduped ledger, because
_load_kalshi_bankroll feeds Kelly sizing: a wrong bankroll silently mis-sizes
every future bet, so this matters beyond bookkeeping.

Usage:  python research\\fix_duplicate_bets.py [--apply]
        (default is a DRY RUN)
"""
import json
import shutil
import sys
from collections import Counter
from datetime import datetime

HIST = r'E:\soccer\data\bet_history.json'
DROPPED = r'E:\soccer\data\bet_history_duplicates_removed.json'
BANKROLLS = {
    'b365': r'E:\soccer\data\bankroll.json',
    'kalshi': r'E:\soccer\data\kalshi_bankroll.json',
    'poly': r'E:\soccer\data\poly_bankroll.json',
}
START = 1000.0


def _f(x):
    try:
        return float(x)
    except (TypeError, ValueError):
        return None


def key(b):
    return (str(b.get('date'))[:10], b.get('home'), b.get('away'),
            str(b.get('bet')), b.get('strategy') or 'baseline')


def main():
    apply = '--apply' in sys.argv
    raw = json.load(open(HIST, encoding='utf-8'))
    rows = raw if isinstance(raw, list) else raw.get('bets', raw)

    seen = Counter()
    keep, dropped = [], []
    for b in rows:
        k = key(b)
        seen[k] += 1
        (dropped if seen[k] > 1 else keep).append(b)

    print(f'{len(rows)} rows -> keep {len(keep)}, drop {len(dropped)}')
    dbl = 0.0
    for b in dropped:
        print(f"   DROP {b.get('date')} {b.get('strategy') or 'baseline':10s} "
              f"{b.get('home')} v {b.get('away')} [{b.get('bet')}]")
        for f in ('pnl', 'kalshi_pnl', 'poly_pnl'):
            v = _f(b.get(f))
            if v is not None:
                dbl += v
                print(f'        {f} {v:+.2f}')
    print(f'\nP&L removed from the ledger: {dbl:+.2f}')

    # SURGICAL adjustment, NOT a replay from $1,000.
    #
    # bet_history.json is a WINDOW (2026-08-01 onward, 124 rows), not the full
    # record -- the forward test alone counts 234 xlow bets. Its running
    # `bankroll_after` values are correct and continuous with the bankroll
    # files (the last row matched to the cent), they simply do not start at
    # $1,000 inside this window. Replaying from START over a partial ledger
    # produced -$666 on Kalshi to correct a -$1.67 duplicate: it would have
    # destroyed three correct bankrolls. So instead, back out exactly the P&L
    # of the dropped rows and shift only what follows them.
    delta = {'b365': 0.0, 'kalshi': 0.0, 'poly': 0.0}
    for b in dropped:
        for ven, pf in (('b365', 'pnl'), ('kalshi', 'kalshi_pnl'), ('poly', 'poly_pnl')):
            v = _f(b.get(pf))
            if v is not None:
                delta[ven] -= v          # remove a spurious loss => bankroll rises

    dropped_dates = sorted(str(b.get('date'))[:10] for b in dropped)
    first_bad = dropped_dates[0] if dropped_dates else None
    for r in keep:
        if first_bad and str(r.get('date'))[:10] >= first_bad:
            for ven, bf in (('b365', 'bankroll_after'),
                            ('kalshi', 'kalshi_bankroll_after'),
                            ('poly', 'poly_bankroll_after')):
                v = _f(r.get(bf))
                if v is not None and delta[ven]:
                    r[bf] = round(v + delta[ven], 2)

    print('\nbankroll adjustment (surgical -- backs out the dropped rows only):')
    run = {}
    for ven in ('b365', 'kalshi', 'poly'):
        try:
            cur = json.load(open(BANKROLLS[ven], encoding='utf-8'))['bankroll']
        except Exception:
            cur = 0.0
        run[ven] = round(cur + delta[ven], 2)
        print(f'   {ven:7s} {cur:>10,.2f}  ->  {run[ven]:>10,.2f}   ({delta[ven]:+,.2f})')

    if not apply:
        print('\nDRY RUN -- nothing written. Re-run with --apply.')
        return 0

    stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    shutil.copy2(HIST, f'{HIST}.bak_{stamp}')
    for p in BANKROLLS.values():
        try:
            shutil.copy2(p, f'{p}.bak_{stamp}')
        except OSError:
            pass
    json.dump(dropped, open(DROPPED, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
    out = keep if isinstance(raw, list) else {**raw, 'bets': keep}
    json.dump(out, open(HIST, 'w', encoding='utf-8'), indent=2, ensure_ascii=False)
    for ven, p in BANKROLLS.items():
        json.dump({'bankroll': round(run[ven], 2),
                   'updated': datetime.now().strftime('%Y-%m-%d %H:%M')},
                  open(p, 'w', encoding='utf-8'), indent=2)
    print(f'\nbackups .bak_{stamp}; dropped rows -> {DROPPED}')
    return 0


if __name__ == '__main__':
    sys.exit(main())
