"""Independently verify the published suit census; Python 3.9+, standard library.

Run beside the downloaded JSON/CSV files, from any current directory. This
checker does not import or execute generate.py. It counts ordered suit draws,
with each draw weighted by the remaining cards of its suit, then divides by
the number of orderings of an unordered hand. Checks use explicit exceptions
and therefore also run under python -O.
"""
import copy
import csv
from decimal import Decimal, localcontext
import io
from itertools import product
import json
from math import factorial
from pathlib import Path

ROOT = Path(__file__).resolve().parent
FIELDS = ['cards', 'shape', 'same_suit_pairs', 'flush_capable_suits',
          'hands', 'total_hands', 'percent']
FIXTURES = [
    ('three-paired-suits', ['As', 'Ks', 'Ah', 'Kh', 'Ad', 'Kd'], '2-2-2', 3, ['s', 'h', 'd']),
    ('two-three-card-suits', ['As', 'Ks', 'Qs', 'Ah', 'Kh', 'Qh'], '3-3', 6, ['s', 'h']),
    ('one-six-card-suit', ['As', 'Ks', 'Qs', 'Js', 'Ts', '9s'], '6', 15, ['s']),
    ('five-card-221', ['As', 'Ks', 'Ah', 'Kh', 'Qd'], '2-2-1', 2, ['s', 'h']),
    ('five-card-32', ['As', 'Ks', 'Qs', 'Ah', 'Kh'], '3-2', 4, ['s', 'h']),
    ('same-shape-ace-paired', ['As', 'Ks', 'Qh', 'Jh', 'Td', '9c'], '2-2-1-1', 2, ['s', 'h']),
    ('same-shape-ace-alone', ['As', 'Kh', 'Qh', 'Jd', 'Td', '9c'], '2-2-1-1', 2, ['h', 'd']),
]


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


def independent_rows():
    answer = []
    for n, total, expected_shapes in [(4, 270725, 5), (5, 2598960, 6), (6, 20358520, 9)]:
        counts = {}
        for sequence in product('shdc', repeat=n):
            used = {s: 0 for s in 'shdc'}
            ways = 1
            for suit in sequence:
                ways *= 13 - used[suit]
                used[suit] += 1
            vector = tuple(sorted(used.values(), reverse=True))
            counts[vector] = counts.get(vector, 0) + ways
        require(len(counts) == expected_shapes, 'Unexpected number of shapes')
        local_total = 0
        for vector, ordered in sorted(counts.items()):
            require(ordered % factorial(n) == 0, 'Nonintegral unordered count')
            hands = ordered // factorial(n)
            local_total += hands
            with localcontext() as ctx:
                ctx.prec = 40
                percent = format(Decimal(100 * hands) / Decimal(total), '.6f')
            answer.append(dict(zip(FIELDS, [n, '-'.join(str(x) for x in vector if x),
                sum(k * (k - 1) // 2 for k in vector), sum(k >= 2 for k in vector),
                hands, total, percent])))
        require(local_total == total, 'Physical hand totals do not reconcile')
    return answer


def fixture_oracle():
    answer = []
    for name, cards, shape, pair_count, suits in FIXTURES:
        require(len(set(cards)) == len(cards), 'Oracle contains duplicate cards')
        require(all(len(c) == 2 and c[0] in '23456789TJQKA' and c[1] in 'shdc'
                    for c in cards), 'Oracle contains invalid cards')
        # Index loops preserve the public witness order without combinations().
        pairs = [[cards[i], cards[j]] for i in range(len(cards))
                 for j in range(i + 1, len(cards)) if cards[i][1] == cards[j][1]]
        occupied = [sum(c[1] == s for c in cards) for s in 'shdc']
        require('-'.join(str(k) for k in sorted(occupied, reverse=True) if k) == shape,
                'Literal shape oracle disagreement')
        require(len(pairs) == pair_count, 'Literal pair-count oracle disagreement')
        require([s for s, k in zip('shdc', occupied) if k >= 2] == suits,
                'Literal eligible-suit oracle disagreement')
        answer.append({'id': name, 'cards': cards, 'shape': shape,
                       'same_suit_pairs': pair_count, 'flush_capable_suits': suits,
                       'pair_witnesses': pairs})
    require([c[0] for c in FIXTURES[-2][1]] == [c[0] for c in FIXTURES[-1][1]],
            'Same-shape comparison must preserve ranks')
    require('s' in FIXTURES[-2][4] and 's' not in FIXTURES[-1][4],
            'Same-shape comparison must change ace-suit eligibility')
    return answer


def verify_data(data, fixtures, expected_rows, expected_examples):
    require(set(data) == {'model', 'method', 'seed', 'random_samples', 'rows', 'examples'},
            'Unexpected result schema')
    require(data['model'] == 'One uniformly random unordered hand from 52 distinct standard cards; no known cards, action filters, board or opponent condition.',
            'Model scope changed')
    require(data['method'] == 'Enumerate labeled suit-count vectors and weight each by product of C(13,count). Exact integer counts; percentages rounded to six decimals.',
            'Published method changed')
    require(data['seed'] is None and type(data['random_samples']) is int
            and data['random_samples'] == 0, 'Must be exact enumeration, not random samples')
    require(data['rows'] == expected_rows, 'Census rows differ from independent exact oracle')
    require(data['examples'] == expected_examples, 'Examples differ from independent fixture oracle')
    require(fixtures == [{'id': r['id'], 'cards': r['cards']} for r in expected_examples],
            'Input fixtures differ from literal oracle')
    for row in data['rows']:
        require(set(row) == set(FIELDS), 'Row keys changed')
        require(all(type(row[k]) is int for k in FIELDS if k not in ('shape', 'percent')),
                'Count fields must be integers')
        require(type(row['shape']) is str and type(row['percent']) is str,
                'Shape and formatted percentage must be strings')


def main():
    data = json.loads((ROOT / 'results.json').read_text(encoding='utf-8'))
    fixtures = json.loads((ROOT / 'examples.json').read_text(encoding='utf-8'))
    rows, examples = independent_rows(), fixture_oracle()
    verify_data(data, fixtures, rows, examples)
    output = io.StringIO(newline='')
    writer = csv.DictWriter(output, fieldnames=FIELDS, lineterminator='\n')
    writer.writeheader()
    writer.writerows(rows)
    require((ROOT / 'suit-patterns.csv').read_bytes() == output.getvalue().encode('utf-8'),
            'CSV bytes differ from independent rows')
    # Self-test that the verification gate actually rejects representative damage.
    changes = [
        lambda d: d['rows'][0].__setitem__('hands', d['rows'][0]['hands'] + 1),
        lambda d: d['rows'][0].__setitem__('percent', '0.000000'),
        lambda d: d['rows'][0].__setitem__('same_suit_pairs', 1),
        lambda d: d['rows'][0].__setitem__('flush_capable_suits', 1),
        lambda d: d['rows'][0].__setitem__('cards', True),
        lambda d: d['rows'].pop(),
        lambda d: d['rows'].append(d['rows'][0]),
        lambda d: d['examples'][0].__setitem__('shape', '3-3'),
        lambda d: d['examples'][0]['pair_witnesses'].pop(),
        lambda d: d['examples'][0]['pair_witnesses'].append(['As', 'Ah']),
        lambda d: d.__setitem__('random_samples', 1),
        lambda d: d.__setitem__('model', 'independent replacement draws'),
    ]
    for change in changes:
        altered = copy.deepcopy(data)
        change(altered)
        try:
            verify_data(altered, fixtures, rows, examples)
        except ValueError:
            continue
        raise RuntimeError('A tampered result was accepted')
    changed_fixtures = copy.deepcopy(fixtures)
    changed_fixtures[0]['cards'][0] = 'Ks'
    try:
        verify_data(data, changed_fixtures, rows, examples)
    except ValueError:
        pass
    else:
        raise RuntimeError('A corrupted input fixture was accepted')
    print('PASS: 20 exact census rows, 7 literal/derived fixtures, CSV byte parity, '
          '5376 weighted suit sequences, 13 tamper rejections; no generator import.')


if __name__ == '__main__':
    main()
