"""Synthetic teaching calculations. No observed rounds, bets or network calls."""
import csv
import json
import logging
from collections import defaultdict
from functools import lru_cache
from pathlib import Path

OUT=Path(__file__).resolve().parent
logging.basicConfig(level=logging.INFO,format='%(message)s')
RANKS=tuple(range(1,11))
P={r:(4/13 if r==10 else 1/13) for r in RANKS}

def add(total,soft,rank):
    if rank==1:
        total+=11;soft+=1
    else:total+=rank
    while total>21 and soft:
        total-=10;soft-=1
    return total,soft

@lru_cache(None)
def dealer_finish(total,soft):
    if total>21:return ((22,1.0),)
    if total>=17:return ((total,1.0),)
    result=defaultdict(float)
    for rank in RANKS:
        for end,probability in dealer_finish(*add(total,soft,rank)):
            result[end]+=P[rank]*probability
    return tuple(sorted(result.items()))

@lru_cache(None)
def dealer(up):
    first=add(0,0,up)
    forbidden=10 if up==1 else 1 if up==10 else None
    remaining=1-P[forbidden] if forbidden else 1
    result=defaultdict(float)
    for hole in RANKS:
        if hole==forbidden:continue
        for end,probability in dealer_finish(*add(*first,hole)):
            result[end]+=P[hole]/remaining*probability
    assert abs(sum(result.values())-1)<1e-12
    return tuple(sorted(result.items()))

@lru_cache(None)
def stand(total,up):
    if total>21:return -1.0
    return sum(probability*(1 if end==22 or total>end else 0 if total==end else -1) for end,probability in dealer(up))

@lru_cache(None)
def play(total,soft,up,policy,can_double=False):
    if total>21:return -1.0
    value=stand(total,up)
    allowed=policy=='optimal' or soft>0 or total<12
    if allowed and total<21:
        value=max(value,sum(P[r]*play(*add(total,soft,r),up,policy,False) for r in RANKS))
    if can_double and allowed:
        value=max(value,2*sum(P[r]*stand(add(total,soft,r)[0],up) for r in RANKS))
    return value

@lru_cache(None)
def split_pending(rank,pending,budget,up,policy):
    if pending==0:return 0.0
    future=split_pending(rank,pending-1,budget,up,policy)
    total=0.0
    for drawn in RANKS:
        state=add(*add(0,0,rank),drawn)
        hand=stand(state[0],up) if rank==1 else play(*state,up,policy,True)
        value=hand+future
        if drawn==rank and rank!=1 and budget>0:
            value=max(value,split_pending(rank,pending+1,budget-1,up,policy))
        total+=P[drawn]*value
    return total

def initial(a,b,up,policy):
    state=add(*add(0,0,a),b)
    chance_bj=P[10] if up==1 else P[1] if up==10 else 0
    if {a,b}=={1,10}:return (1-chance_bj)*1.5
    value=play(*state,up,policy,True)
    if a==b:value=max(value,split_pending(a,2,2,up,policy))
    return -chance_bj+(1-chance_bj)*value

def controls():
    assert add(20,1,10)==(20,0)
    assert add(12,1,1)==(13,1)
    assert add(21,1,1)==(12,0)
    assert abs(sum(P.values())-1)<1e-12
    # Independent forward probability propagation, without dealer_finish().
    for up in RANKS:
        blocked=10 if up==1 else 1 if up==10 else None
        denominator=1-P[blocked] if blocked else 1
        active=defaultdict(float)
        for hole in RANKS:
            if hole!=blocked:active[add(*add(0,0,up),hole)]+=P[hole]/denominator
        ended=defaultdict(float)
        for _ in range(64):
            following=defaultdict(float)
            for (total,soft),mass in active.items():
                if total>21:ended[22]+=mass
                elif total>=17:ended[total]+=mass
                else:
                    for rank in RANKS:following[add(total,soft,rank)]+=mass*P[rank]
            active=following
            if not active:break
        assert not active
        assert all(abs(ended[end]-probability)<1e-12 for end,probability in dealer(up))
    # Separate fixed-policy forward recursion for hard-hand hit/stand control.
    def hit_tree(total,soft,up):
        if total>21:return -1.0
        if total>=17:return stand(total,up)
        return sum(P[r]*hit_tree(*add(total,soft,r),up) for r in RANKS)
    assert abs(play(11,0,6,'optimal',True)-2*sum(P[r]*stand(add(11,0,r)[0],6) for r in RANKS))<1e-12
    assert hit_tree(16,0,10)>stand(16,10)

def main():
    controls()
    sums={policy:sum(P[up]*P[a]*P[b]*initial(a,b,up,policy) for up in RANKS for a in RANKS for b in RANKS) for policy in ('optimal','never-bust')}
    # Independent published reference for exactly these infinite-deck rules:
    # wizardofodds.com/games/blackjack/expected-return-infinite-deck/
    assert abs(sums['optimal']-(-.00511734))<1e-8
    assert sums['optimal']>sums['never-bust'] and sums['optimal']<0
    decisions=[]
    for up in (2,6,7,10,1):
        for total in range(12,17):
            hit=sum(P[r]*play(*add(total,0,r),up,'optimal',False) for r in RANKS)
            decisions.append({'hard_total':total,'dealer_upcard':up,'stand_ev':stand(total,up),'hit_then_optimal_ev':hit,'price_of_forced_stand':max(stand(total,up),hit)-stand(total,up)})
    jetx=[{'panels':n,'stake_per_panel':s,'total_at_risk':n*s} for n,s in ((1,5),(2,5),(4,5),(4,1.25))]
    straddle=[{'small_blind':1,'big_blind':2,'posted_extra_blinds':posted,'current_live_unit':posted[-1] if posted else 2,'effective_starting_stack':200,'stack_in_original_bb':100,'stack_in_current_units':200/(posted[-1] if posted else 2),'unopened_pot':3+sum(posted)} for posted in ([],[4],[4,8])]
    result={'blackjack':{'model':'Infinite-deck iid rank draws, exact expectation by recursion; not a finite-shoe or observed casino result','rules':{'dealer':'S17','peek':True,'blackjack_payout':'3:2','double':'any initial two cards; DAS','split':'max four hands, no resplit aces, one card to split aces','surrender':False,'insurance':False},'policies':{'optimal':'Maximize legal stand/hit/double/split EV','never-bust':'Same optimization but no hit or double on hard totals 12 or more'},'net_ev_per_initial_unit':sums,'cost_per_100_initial_units':{k:-v*100 for k,v in sums.items()},'additional_cost_per_100_initial_units':(sums['optimal']-sums['never-bust'])*100,'decisions':decisions},'jetx':{'source_rules_version':'v.2.2.0.0','probability_distribution':'Not inferred','exposure_examples':jetx,'same_target_example':{'stakes':[1,1,1,1],'cashout_target':2,'all_success_net':4,'all_fail_net':-4,'note':'Shared round threshold; ignores execution and caps, not four independent draws'}},'straddle':{'unit_examples':straddle,'example_raise':{'opening_to':12,'one_caller':True,'blinds_and_straddle':7,'flop_pot':31,'remaining_effective_stack':188,'spr':188/31},'availability_at_specific_operator':'UNVERIFIED'}}
    result['straddle']['pot_examples']=[]
    for name,contributions in [('nonblind',[12,12,1,2,4]),('straddler',[12,12,1,2])]:
        pot=sum(contributions);behind=200-12
        result['straddle']['pot_examples'].append({'caller':name,'final_contributions':contributions,'flop_pot':pot,'stack_behind':behind,'spr':behind/pot})
    assert [r['flop_pot'] for r in result['straddle']['pot_examples']]==[31,27]
    assert [r['unopened_pot'] for r in straddle]==[3,7,15]
    result['jetx']['mixed_target_example']={'stakes':[1,1],'gross_payouts':[1.5,0],'net_result':sum([1.5,0])-sum([1,1])}
    result['jetx']['twenty_round_turnover']={'one_total_unit':20*1,'four_one_unit_panels':20*4}
    result['blackjack']['hundred_ten_unit_initial_bets']={k:-v*100*10 for k,v in sums.items()}
    (OUT/'calculations').mkdir(exist_ok=True)
    (OUT/'calculations/results.json').write_text(json.dumps(result,indent=2),encoding='utf-8')
    with (OUT/'calculations/blackjack-decisions.csv').open('w',newline='',encoding='utf-8') as file:
        writer=csv.DictWriter(file,fieldnames=list(decisions[0]));writer.writeheader();writer.writerows(decisions)
    logging.info('Model EV: %s; additional expected cost / 100 initial units: %.8f',sums,result['blackjack']['additional_cost_per_100_initial_units'])

if __name__=='__main__':main()
