"""Exact synthetic river range aggregation. Python 3.11+, standard library only.
Run from any directory; files are written beside this script. No random sampling.
"""
import csv
import json
from collections import Counter
from fractions import Fraction as F
from itertools import combinations
from pathlib import Path

ROOT = Path(__file__).resolve().parent
BOARD = ['2c', '3d', '7h', '9s', 'Jc']
HERO = [('AsAh', '1'), ('KsKh', '1')]
VILLAIN = [('AsAd', '1'), ('AcAd', '1'), ('QsQh', '1')]
CASES = [
    ('equal_inputs', BOARD, HERO, VILLAIN),
    ('half_qq', BOARD, HERO, [('AsAd','1'),('AcAd','1'),('QsQh','1/2')]),
    ('half_hero_aa', BOARD, [('AsAh','1/2'),('KsKh','1')], VILLAIN),
    ('all_compatible', BOARD, HERO, [('AcAd','1'),('QsQh','1')]),
    ('zero_row', BOARD, HERO, [('AsKd','1')]),
    ('zero_total', BOARD, [('AsAh','1')], [('AsKd','1')]),
    ('all_ties', ['As','Ks','Qs','Js','Ts'], [('2c2d','1'),('3c3d','1')], [('4c4d','1'),('5c5d','1')]),
]

def cards(hand):
    return [hand[:2], hand[2:]]

def five_rank(hand):
    ranks = sorted(['23456789TJQKA'.index(c[0]) + 2 for c in hand], reverse=True)
    groups = sorted(((n, r) for r, n in Counter(ranks).items()), reverse=True)
    flush = len({c[1] for c in hand}) == 1
    unique = sorted(set(ranks), reverse=True)
    straight = unique[0] if len(unique) == 5 and unique[0]-unique[4] == 4 else (5 if unique == [14,5,4,3,2] else 0)
    if flush and straight: return (8, straight)
    if groups[0][0] == 4: return (7, groups[0][1], groups[1][1])
    if [g[0] for g in groups] == [3,2]: return (6, groups[0][1], groups[1][1])
    if flush: return (5, *ranks)
    if straight: return (4, straight)
    if groups[0][0] == 3: return (3, groups[0][1], *sorted((r for n,r in groups[1:]), reverse=True))
    if [g[0] for g in groups[:2]] == [2,2]: return (2, *sorted((groups[0][1],groups[1][1]), reverse=True), groups[2][1])
    if groups[0][0] == 2: return (1, groups[0][1], *sorted((r for n,r in groups[1:]), reverse=True))
    return (0, *ranks)

def rank(hand):
    return max(five_rank(c) for c in combinations(hand, 5))

def fraction(value):
    return None if value is None else str(value)

def generate():
    result = {'schema':1, 'method':'Exact fixed-river legal-pair enumeration; product input weights conditioned on legality.', 'cases':[]}
    pair_rows, hand_rows = [], []
    for case_id, board, hero, villain in CASES:
        if len(set(board)) != 5: raise ValueError('Duplicate board card')
        for range_ in (hero, villain):
            if len({h for h,w in range_}) != len(range_): raise ValueError('Duplicate combo')
            for h,w in range_:
                if len(set(board+cards(h))) != 7 or F(w) < 0: raise ValueError('Invalid input')
        pairs, hands = [], []
        total = score = F(0)
        for h,a in hero:
            mass = earned = F(0)
            for v,b in villain:
                collision = sorted(set(cards(h)) & set(cards(v)))
                legal = not collision
                weight = F(a)*F(b) if legal else F(0)
                share = None
                if legal:
                    hr,vr = rank(board+cards(h)),rank(board+cards(v))
                    share = F(1) if hr > vr else F(0) if hr < vr else F(1,2)
                    mass += weight
                    earned += weight*share
                pair = {'hero':h,'villain':v,'legal':legal,'collision':' '.join(collision),'weight':fraction(weight),'hero_share':fraction(share)}
                pairs.append(pair)
                pair_rows.append({'case':case_id,**pair})
            hands.append({'hero':h,'input_weight':a,'mass':fraction(mass),'equity':fraction(earned/mass) if mass else None})
            total += mass
            score += earned
        for h in hands:
            h['marginal'] = fraction(F(h['mass'])/total) if total else None
            hand_rows.append({'case':case_id,**h})
        # The deliberately naive comparator uses input weights, ignoring legal-pair mass.
        supported = [h for h in hands if F(h['mass']) > 0]
        naive = sum((F(h['input_weight'])*F(h['equity']) for h in supported), F(0))/sum((F(h['input_weight']) for h in supported), F(0)) if supported else None
        result['cases'].append({'id':case_id,'board':board,'hero_inputs':hero,'villain_inputs':villain,'pairs':pairs,'hands':hands,'total_mass':fraction(total),'hero_score':fraction(score),'equity':fraction(score/total) if total else None,'naive_input_weighted_equity':fraction(naive)})
    (ROOT/'results.json').write_text(json.dumps(result, indent=2)+'\n', encoding='utf-8', newline='\n')
    for name,rows in [('pairs.csv',pair_rows),('hands.csv',hand_rows)]:
        with (ROOT/name).open('w', encoding='utf-8', newline='') as stream:
            writer = csv.DictWriter(stream, fieldnames=list(rows[0]), lineterminator='\n')
            writer.writeheader(); writer.writerows(rows)
    print(json.dumps({c['id']:{k:c[k] for k in ['total_mass','equity','naive_input_weighted_equity']} for c in result['cases']}, indent=2))

if __name__ == '__main__':
    generate()
