"""Verify the companion ICM data using independent finish-order enumeration.

Python 3 standard library only. Put this file beside results.json and the two
sensitivity CSVs, then run: python verify.py
The files are read, never changed. Any mismatch exits with a failure.
The generator uses a three-player closed form; this verifier instead enumerates
all possible finishing orders using exact Fraction arithmetic.
"""
from fractions import Fraction
from itertools import permutations
from pathlib import Path
import csv
import json
import math
import sys

ROOT = Path(__file__).resolve().parent
F = Fraction


def require(condition, message):
    if not condition:
        raise ValueError(message)


def finish_order_equity(stacks, prizes):
    require(len(stacks) == len(prizes) == 3, 'Exactly three players required')
    require(all(type(s) is int and s >= 0 for s in stacks), 'Invalid stacks')
    require(sum(stacks) > 0, 'No chips')
    require(all(type(p) is int and p >= 0 for p in prizes), 'Invalid prizes')
    require(prizes == sorted(prizes, reverse=True), 'Invalid prize order')
    alive = [i for i in range(3) if stacks[i] > 0]
    gone = [i for i in range(3) if stacks[i] == 0]
    require(len(gone) <= 1, 'Simultaneous elimination is outside this model')
    ev = [F(0) for _ in stacks]
    total_probability = F(0)
    for order in permutations(alive):
        weight = F(1)
        available = sum(stacks)
        for player in order:
            weight *= F(stacks[player], available)
            available -= stacks[player]
        total_probability += weight
        for rank, player in enumerate(order):
            ev[player] += weight * prizes[rank]
    for player in gone:
        ev[player] = F(prizes[len(alive)])
    require(total_probability == 1, 'Finish-order probabilities do not sum to one')
    require(sum(ev) == sum(prizes), 'Prize conservation failed')
    return ev


def encode(values):
    return [{'exact': str(value), 'dollars': float(value)} for value in values]


def compare(actual, expected, path):
    require(type(actual) is type(expected), path + ': unexpected value type')
    if isinstance(expected, dict):
        require(set(actual) == set(expected), path + ': missing or extra fields')
        for key in expected:
            compare(actual[key], expected[key], path + '.' + key)
    elif isinstance(expected, list):
        require(len(actual) == len(expected), path + ': unexpected item count')
        for index, (a, e) in enumerate(zip(actual, expected)):
            compare(a, e, path + '[' + str(index) + ']')
    else:
        if isinstance(actual, float):
            require(math.isfinite(actual), path + ': nonfinite number')
        require(actual == expected, path + ': expected ' + repr(expected) + ', got ' + repr(actual))


def unique_object(pairs):
    value = {}
    for key, item in pairs:
        require(key not in value, 'Duplicate JSON key: ' + key)
        value[key] = item
    return value


def read_csv(name, columns):
    with (ROOT / name).open(encoding='utf-8', newline='') as handle:
        reader = csv.DictReader(handle)
        require(reader.fieldnames == columns, name + ': header mismatch')
        return list(reader)


def verify():
    prizes = [500, 300, 200]
    states = {'before': [20, 60, 20], 'large_wins': [20, 80, 0], 'short_wins': [20, 40, 40]}
    values = {name: finish_order_equity(chips, prizes) for name, chips in states.items()}
    baseline = values['before'][0]
    fair = [(win + loss) / 2 for win, loss in zip(values['large_wins'], values['short_wins'])]
    changes = [after - before for after, before in zip(fair, values['before'])]
    require(sum(fair) == sum(prizes) and sum(changes) == 0, 'Mean conservation failed')
    break_even = (baseline - values['short_wins'][0]) / (values['large_wins'][0] - values['short_wins'][0])
    biased = F(1, 10) * values['large_wins'][0] + F(9, 10) * values['short_wins'][0]
    expected = {
        'question': 'Can an unchanged spectator stack lose ICM equity in one outcome?',
        'players': ['Hero', 'A', 'B'],
        'stack_unit': '1000 chips',
        'prizes_dollars': prizes,
        'states': {name: {'stacks': chips, 'equities': encode(values[name])} for name, chips in states.items()},
        'fair_large_win_probability': '1/2',
        'fair_expected_equities': encode(fair),
        'fair_expected_changes': encode(changes),
        'break_even_large_win_probability': str(break_even),
        'biased_large_win_probability': '1/10',
        'biased_hero_equity': encode([biased])[0],
    }
    actual = json.loads((ROOT / 'results.json').read_text(encoding='utf-8'), object_pairs_hook=unique_object)
    compare(actual, expected, 'results.json')

    probability_rows = []
    for step in range(101):
        probability = F(step, 100)
        hero = probability * values['large_wins'][0] + (1 - probability) * values['short_wins'][0]
        probability_rows.append({
            'large_win_probability': str(probability),
            'hero_equity_exact_dollars': str(hero),
            'hero_equity_dollars': format(float(hero), '.8f'),
            'change_exact_dollars': str(hero - baseline),
        })
    compare(read_csv('probability-sensitivity.csv', list(probability_rows[0])), probability_rows, 'probability-sensitivity.csv')

    transfer_rows = []
    profiles = {'tiered': [500, 300, 200], 'winner_take_all': [1000, 0, 0], 'equal': [300, 300, 300]}
    for name, payout in profiles.items():
        before = finish_order_equity([20, 60, 20], payout)[0]
        for amount in range(21):
            left = finish_order_equity([20, 60 + amount, 20 - amount], payout)
            right = finish_order_equity([20, 60 - amount, 20 + amount], payout)
            average = [(a + b) / 2 for a, b in zip(left, right)]
            require(sum(average) == sum(payout), 'Transfer mean conservation failed')
            if name != 'tiered':
                require(average[0] == before, 'Invariant payout control failed')
            elif amount > 0:
                require(average[0] > before, 'Fair positive-transfer check failed')
            transfer_rows.append({
                'profile': name,
                'transfer_1000_chips': str(amount),
                'hero_before_exact_dollars': str(before),
                'hero_fair_mean_exact_dollars': str(average[0]),
                'hero_change_exact_dollars': str(average[0] - before),
            })
    compare(read_csv('transfer-sensitivity.csv', list(transfer_rows[0])), transfer_rows, 'transfer-sensitivity.csv')
    print('PASS: every JSON field, 101 probability rows and 63 transfer/payout rows match.')
    print('Exact values, decimal displays, configuration, finish-order mass and prize conservation verified.')


if __name__ == '__main__':
    try:
        verify()
    except (ValueError, KeyError, TypeError, OSError, csv.Error, ArithmeticError) as error:
        print('FAIL: ' + str(error), file=sys.stderr)
        sys.exit(1)
