#!/usr/bin/env python3
"""Exact, standard-library Hold'em river casebook. Python 3.10+; no network."""
import csv
import hashlib
import io
import json
from collections import Counter
from itertools import combinations
from pathlib import Path

ROOT = Path(__file__).resolve().parent
RANKS = '23456789TJQKA'
SUITS = 'cdhs'
DECK = tuple(r + s for r in RANKS for s in SUITS)
NAMES = ('high card', 'one pair', 'two pair', 'three of a kind', 'straight',
         'flush', 'full house', 'four of a kind', 'straight flush')


def rank5(cards):
    values = [RANKS.index(c[0]) + 2 for c in cards]
    groups = sorted(((n, r) for r, n in Counter(values).items()), reverse=True)
    descending = sorted(values, reverse=True)
    distinct = sorted(set(values))
    straight = (5 if distinct == [2, 3, 4, 5, 14] else
                max(distinct) if len(distinct) == 5 and max(distinct) - min(distinct) == 4 else 0)
    flush = len({c[1] for c in cards}) == 1
    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, *descending)
    if straight:
        return (4, straight)
    if groups[0][0] == 3:
        return (3, groups[0][1], *sorted((r for n, r in groups if n == 1), reverse=True))
    if [g[0] for g in groups[:2]] == [2, 2]:
        return (2, *sorted((r for n, r in groups if n == 2), reverse=True), groups[2][1])
    if groups[0][0] == 2:
        return (1, groups[0][1], *sorted((r for n, r in groups if n == 1), reverse=True))
    return (0, *descending)


def best(cards):
    if not 5 <= len(cards) <= 7 or len(set(cards)) != len(cards) or any(c not in DECK for c in cards):
        raise ValueError('Use five to seven distinct valid cards')
    return max(rank5(combo) for combo in combinations(cards, 5))


def calculate(spec):
    if spec['version'] != 1 or spec['street'] != 'turn' or len(spec['cases']) != 4:
        raise ValueError('Unsupported casebook specification')
    ids = set()
    rows, summaries = [], []
    for case in spec['cases']:
        if case['id'] in ids:
            raise ValueError('Duplicate case ID')
        ids.add(case['id'])
        hero, opponent, board = case['hero'], case['opponent'], case['board']
        if (len(hero), len(opponent), len(board)) != (2, 2, 4):
            raise ValueError('Expected two hole cards per player and four board cards')
        known = hero + opponent + board
        if len(set(known)) != 8 or any(c not in DECK for c in known):
            raise ValueError('Invalid or duplicate known cards')
        start_hero, start_opponent = best(hero + board), best(opponent + board)
        if start_hero <= start_opponent:
            raise ValueError('Every fixture must start with hero strictly ahead')
        outcomes = {'win': [], 'tie': [], 'loss': []}
        flush_cards = []
        for river in DECK:
            if river in known:
                continue
            h, o = best(hero + board + [river]), best(opponent + board + [river])
            result = 'win' if h > o else 'loss' if h < o else 'tie'
            outcomes[result].append(river)
            if o[0] == 5:
                flush_cards.append(river)
            rows.append({'case_id': case['id'], 'river': river, 'result': result,
                         'hero_category': NAMES[h[0]], 'opponent_category': NAMES[o[0]],
                         'hero_rank': list(h), 'opponent_rank': list(o)})
        summaries.append({'id': case['id'], 'rivers': 44,
                          'turn_hero_category': NAMES[start_hero[0]],
                          'turn_opponent_category': NAMES[start_opponent[0]],
                          'wins': len(outcomes['win']), 'ties': len(outcomes['tie']),
                          'losses': len(outcomes['loss']), 'loss_cards': outcomes['loss'],
                          'tie_cards': outcomes['tie'], 'opponent_flush_cards': flush_cards,
                          'hero_pot_share_fraction': f"{2 * len(outcomes['win']) + len(outcomes['tie'])}/88"})
    return {'method': spec['method'], 'total_rows': len(rows), 'summaries': summaries, 'outcomes': rows}


def main():
    spec = json.loads((ROOT / 'cases.json').read_text(encoding='utf-8'))
    output = calculate(spec)
    (ROOT / 'results.json').write_text(json.dumps(output, indent=2) + '\n', encoding='utf-8', newline='\n')
    stream = io.StringIO(newline='')
    fields = ['case_id', 'river', 'result', 'hero_category', 'opponent_category']
    writer = csv.DictWriter(stream, fieldnames=fields, extrasaction='ignore', lineterminator='\n')
    writer.writeheader()
    writer.writerows(output['outcomes'])
    (ROOT / 'rivers.csv').write_text(stream.getvalue(), encoding='utf-8', newline='\n')
    print(json.dumps(output['summaries'], indent=2))


if __name__ == '__main__':
    main()
