"""Bounded educational2x staking models, independent win probability.485.

Integer units, no fees/deposits, hypothetical cap, no real platform limits.
Blocked progression retains residual capital; it is not automatically zero.
All strategies share each generated path of opportunities. Pattern waits for
three observed losses, bets once on the next opportunity, then resets its watch.
Labouchere resets[1,2,3,4] after completing a list. Paroli resets after3wins.
Usage: python staking_models.py [--initial 100 --cap 64 --strategy all]
"""
import argparse
import json
import logging
import random
from statistics import median

STRATEGIES=('flat','martingale','fibonacci','dalembert','labouchere','pattern','paroli')


def run_path(outcomes, initial=1000, base=1, cap=500, strategy='martingale', milestones=(), ledger=False):
    if strategy not in STRATEGIES or any(not isinstance(x,int) or x<1 for x in (initial,base,cap)):
        raise ValueError('Known strategy and positive integer parameters required')
    bank=initial;bet=base*5 if strategy=='labouchere' else base;executed=turnover=gross=0
    level=win_run=observed_losses=0
    fibonacci=[1,1];cancel=[1,2,3,4]
    blocked=None;snapshots={};rows=[]
    def reason():
        insufficient=bet>bank;over_cap=bet>cap
        return 'funds_and_cap' if insufficient and over_cap else 'funds' if insufficient else 'cap' if over_cap else None
    def state():return dict(balance=bank,executed=executed,turnover=turnover,gross=gross,
                           nextStake=bet,blocked=blocked,zeroBalance=bank==0)
    blocked=reason()
    for opportunity,won in enumerate(outcomes,1):
        if blocked:break
        place=strategy!='pattern' or observed_losses>=3
        if place:
            before=bank;paid=2*bet if won else 0
            bank+=paid-bet;executed+=1;turnover+=bet;gross+=paid
            if ledger:rows.append(dict(opportunity=opportunity,balanceBefore=before,stake=bet,won=won,gross=paid,balanceAfter=bank))
            if strategy=='martingale':bet=base if won else bet*2
            elif strategy=='fibonacci':
                level=max(0,level-2) if won else level+1
                while level>=len(fibonacci):fibonacci.append(fibonacci[-1]+fibonacci[-2])
                bet=base*fibonacci[level]
            elif strategy=='dalembert':
                level=max(0,level-1) if won else level+1;bet=base*(level+1)
            elif strategy=='labouchere':
                if won:cancel=cancel[1:-1] if len(cancel)>1 else []
                else:cancel.append(bet//base)
                if not cancel:cancel=[1,2,3,4]
                bet=base*(cancel[0]+cancel[-1] if len(cancel)>1 else cancel[0])
            elif strategy=='paroli':
                win_run=win_run+1 if won else 0
                if win_run==3:win_run=0
                bet=base*2**win_run
            elif strategy=='pattern':observed_losses=0
            blocked=reason()
        elif strategy=='pattern':observed_losses=0 if won else observed_losses+1
        if opportunity in milestones:snapshots[opportunity]=state()
    assert bank==initial+gross-turnover
    final=state()
    for point in milestones:
        if point<=len(outcomes) and point not in snapshots:snapshots[point]=dict(final)
    return dict(final=final,milestones=snapshots,ledger=rows)


def benchmark(sessions=5000, initial=1000, base=1, cap=500, horizon=500, seed=20261023, strategies=('martingale',)):
    if any(not isinstance(x,int) or x<1 for x in (sessions,horizon)) or any(s not in STRATEGIES for s in strategies):
        raise ValueError('Positive sessions/horizon and known strategies required')
    points=tuple(n for n in (50,100,250,500) if n<=horizon)
    if horizon not in points:points+= (horizon,)
    samples={s:{n:[] for n in points} for s in strategies};rng=random.Random(seed)
    for _ in range(sessions):
        outcomes=[rng.random()<.485 for _ in range(horizon)]
        for strategy in strategies:
            for point,state in run_path(outcomes,initial,base,cap,strategy,points)['milestones'].items():
                samples[strategy][point].append(state)
    results=[]
    for strategy in strategies:
        for point in points:
            states=samples[strategy][point];active=[s['balance'] for s in states if not s['blocked']]
            total_turnover=sum(s['turnover'] for s in states);total_gross=sum(s['gross'] for s in states)
            total_final=sum(s['balance'] for s in states)
            assert total_final==sessions*initial+total_gross-total_turnover
            results.append(dict(strategy=strategy,opportunities=point,sessions=sessions,
                blocked=sum(s['blocked'] is not None for s in states),active=len(active),
                zeroBalances=sum(s['zeroBalance'] for s in states),
                medianFinal=median(s['balance'] for s in states),
                medianActive=median(active) if active else None,
                meanNet=(total_gross-total_turnover)/sessions,
                meanTurnover=total_turnover/sessions,totalGross=total_gross,
                totalTurnover=total_turnover,totalFinal=total_final,
                blockReasons={reason:sum(s['blocked']==reason for s in states) for reason in ('funds','cap','funds_and_cap')}))
    return dict(scope='Synthetic independent2x model; no casino transactions or measured limits',
                seed=seed,pWin=.485,initial=initial,base=base,cap=cap,horizon=horizon,results=results)


if __name__=='__main__':
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--sessions',type=int,default=5000)
    parser.add_argument('--initial',type=int,default=1000)
    parser.add_argument('--base',type=int,default=1)
    parser.add_argument('--cap',type=int,default=500)
    parser.add_argument('--horizon',type=int,default=500)
    parser.add_argument('--seed',type=int,default=20261023)
    parser.add_argument('--strategy',choices=(*STRATEGIES,'all'),default='martingale')
    args=parser.parse_args()
    result=benchmark(args.sessions,args.initial,args.base,args.cap,args.horizon,args.seed,
                     STRATEGIES if args.strategy=='all' else (args.strategy,))
    logging.basicConfig(level=logging.INFO,format='%(message)s')
    logging.info(json.dumps(result,indent=2))
