"""Python 3.10+, standard library. Read-only checks; run beside the public files.

Recomputes scheduled posts with a rotating seat queue, checks literal teaching
oracles and both CSVs, then tests the generator on additional synthetic inputs.
This verifies arithmetic and published files, not an event's rules or strategy.
"""
from collections import deque
from copy import deepcopy
from pathlib import Path
import csv
import importlib.util
import io
import json
import sys

ROOT = Path(__file__).resolve().parent
ROW_FIELDS = ['increase_from_hand', 'hand', 'level', 'button', 'seat', 'role',
              'small_blind', 'big_blind', 'big_blind_ante', 'post', 'cumulative']
SUMMARY_FIELDS = ['increase_from_hand', 'seat', 'first_three_posts', 'all_hand_posts']
MODEL = 'gross full scheduled posts; no pot returns, balances or decision EV'


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


def identical(actual, expected, message):
    # Serialization also distinguishes bool from int, unlike Python equality.
    require(json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True), message)


def validate(config):
    required = {'schema_version', 'description', 'seats_clockwise', 'first_button',
                'hands', 'low', 'high', 'increase_from_hands', 'main_increase_from_hand'}
    require(type(config) is dict and set(config) == required, 'Wrong schedule fields')
    require(type(config['schema_version']) is int and config['schema_version'] == 1,
            'Unsupported schema version')
    require(type(config['description']) is str and bool(config['description'].strip()),
            'Missing description')
    seats = config['seats_clockwise']
    require(type(seats) is list and 3 <= len(seats) <= 9, 'Require 3-9 seats')
    require(all(type(s) is str and bool(s.strip()) for s in seats), 'Invalid seat label')
    require(len(set(seats)) == len(seats), 'Duplicate seat')
    require(type(config['first_button']) is str and config['first_button'] in seats,
            'Unknown button')
    hands = config['hands']
    require(type(hands) is int and 1 <= hands <= 100, 'Invalid hand horizon')
    boundaries = config['increase_from_hands']
    require(type(boundaries) is list and bool(boundaries), 'Missing boundaries')
    require(all(type(b) is int and 1 <= b <= hands + 1 for b in boundaries), 'Invalid boundary')
    require(len(set(boundaries)) == len(boundaries), 'Duplicate boundary')
    require(type(config['main_increase_from_hand']) is int
            and config['main_increase_from_hand'] in boundaries, 'Invalid main boundary')
    for name in ('low', 'high'):
        level = config[name]
        require(type(level) is dict and set(level) == {'small_blind', 'big_blind', 'big_blind_ante'},
                'Invalid level fields')
        require(all(type(v) is int and 0 <= v <= 10**9 for v in level.values()),
                'Invalid chip amount')
        require(0 < level['small_blind'] <= level['big_blind'], 'Invalid blind relationship')
    require(all(config['high'][k] >= config['low'][k] for k in config['low']),
            'Amounts decrease')


def reference(config):
    validate(config)
    rows, summary = [], []
    for boundary in config['increase_from_hands']:
        ring = deque(config['seats_clockwise'])
        while ring[0] != config['first_button']:
            ring.rotate(-1)
        totals = dict.fromkeys(ring, 0)
        first_three = dict.fromkeys(ring, 0)
        for hand in range(1, config['hands'] + 1):
            button, small, big = list(ring)[:3]
            name = 'low' if hand < boundary else 'high'
            level = config[name]
            # Assign obligations to the three leading queue entries directly.
            obligations = {seat: ('-', 0, 0, 0) for seat in ring}
            obligations[button] = ('BTN', 0, 0, 0)
            obligations[small] = ('SB', level['small_blind'], 0, 0)
            obligations[big] = ('BB', 0, level['big_blind'], level['big_blind_ante'])
            for seat in config['seats_clockwise']:
                role, sb, bb, ante = obligations[seat]
                posted = sum((sb, bb, ante))
                totals[seat] += posted
                if hand <= 3:
                    first_three[seat] += posted
                rows.append(dict(zip(ROW_FIELDS, (boundary, hand, name, button, seat,
                                                  role, sb, bb, ante, posted, totals[seat]))))
            ring.rotate(-1)
        summary.extend(dict(zip(SUMMARY_FIELDS, (boundary, seat, first_three[seat], totals[seat])))
                       for seat in config['seats_clockwise'])
    return {'units': 'chips', 'model': MODEL, 'rows': rows, 'summary': summary}


def expected_csv(rows, fields):
    stream = io.StringIO(newline='')
    writer = csv.writer(stream, lineterminator='\n')
    writer.writerow(fields)
    writer.writerows([row[field] for field in fields] for row in rows)
    return stream.getvalue().encode('utf-8')


def verify_outputs(config, result, posts, summary):
    expected = reference(config)
    identical(result, expected, 'JSON output differs from independent seat rotation')
    require(posts == expected_csv(expected['rows'], ROW_FIELDS), 'posts.csv differs')
    require(summary == expected_csv(expected['summary'], SUMMARY_FIELDS), 'summary.csv differs')


def rejects(action, name):
    try:
        action()
    except (ValueError, TypeError, KeyError):
        return
    raise ValueError('Expected rejection: ' + name)


def main():
    config = json.loads((ROOT / 'schedule.json').read_text(encoding='utf-8'))
    result = json.loads((ROOT / 'results.json').read_text(encoding='utf-8'))
    posts = (ROOT / 'posts.csv').read_bytes()
    summary = (ROOT / 'summary.csv').read_bytes()
    # Freeze the public teaching fixture, so changing all outputs consistently
    # cannot silently invalidate the article's literal examples.
    baseline = {
        'schema_version': 1,
        'description': 'Synthetic full-post obligations, not a stack or strategy simulation.',
        'seats_clockwise': list('ABCDEF'), 'first_button': 'A', 'hands': 6,
        'low': {'small_blind': 500, 'big_blind': 1000, 'big_blind_ante': 1000},
        'high': {'small_blind': 1000, 'big_blind': 2000, 'big_blind_ante': 2000},
        'increase_from_hands': list(range(1, 8)), 'main_increase_from_hand': 3,
    }
    identical(config, baseline, 'Published teaching fixture changed')
    verify_outputs(config, result, posts, summary)
    require(len(result['rows']) == 252 and len(result['summary']) == 42, 'Wrong output counts')
    main_rows = [r for r in result['rows'] if r['increase_from_hand'] == 3]
    for seat, literal in {'C': [2000, 500, 0, 0, 0, 0],
                          'E': [0, 0, 4000, 1000, 0, 0]}.items():
        identical([r['post'] for r in main_rows if r['seat'] == seat], literal,
                  'Literal seat oracle failed: ' + seat)
    main_totals = {r['seat']: r['all_hand_posts'] for r in result['summary']
                   if r['increase_from_hand'] == 3}
    identical(main_totals, dict(zip('ABCDEF', [5000, 4500, 2500, 3000, 5000, 5000])),
              'Literal six-hand totals failed')
    later_e = next(r for r in result['summary'] if r['increase_from_hand'] == 4 and r['seat'] == 'E')
    require(later_e['first_three_posts'] == 2000, 'Boundary4 E oracle failed')
    require(all(r['all_hand_posts'] == 2500 for r in result['summary']
                if r['increase_from_hand'] == 7), 'Constant-level orbit invariant failed')

    sys.dont_write_bytecode = True
    spec = importlib.util.spec_from_file_location('public_blind_schedule_generator', ROOT / 'generate.py')
    generator = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(generator)
    identical(generator.calculate(config), result, 'Generator disagrees on baseline')
    custom_count = 0
    for n in range(3, 10):
        for first in range(n):
            custom = deepcopy(config)
            custom['seats_clockwise'] = [f'S{i}' for i in range(n)]
            custom['first_button'] = custom['seats_clockwise'][first]
            custom['hands'] = 2 * n
            custom['increase_from_hands'] = [1, n, n + 1, 2 * n + 1]
            custom['main_increase_from_hand'] = n
            expected = reference(custom)
            identical(generator.calculate(custom), expected, '3-9 seat reference mismatch')
            require(all(r['all_hand_posts'] == 5000 for r in expected['summary']
                        if r['increase_from_hand'] == 2 * n + 1), 'Two low orbits differ by seat')
            rotated = deepcopy(custom)
            rotated['seats_clockwise'] = rotated['seats_clockwise'][1:] + rotated['seats_clockwise'][:1]
            actual_rotation = generator.calculate(rotated)
            key = lambda r: tuple(r[k] for k in ('increase_from_hand', 'hand', 'seat'))
            identical(sorted(actual_rotation['rows'], key=key), sorted(expected['rows'], key=key),
                      'Changing seat-list origin changed physical rotation')
            scaled = deepcopy(custom)
            for level in ('low', 'high'):
                scaled[level] = {k: v * 3 for k, v in scaled[level].items()}
            scaled_result = generator.calculate(scaled)
            identical(scaled_result, reference(scaled), 'Scale reference mismatch')
            require(all(b['post'] == 3 * a['post'] and b['cumulative'] == 3 * a['cumulative']
                        for a, b in zip(expected['rows'], scaled_result['rows'])), 'Scale invariant failed')
            custom_count += 3

    for n in range(3, 10):
        edge = deepcopy(config)
        edge['seats_clockwise'] = [f'E{i}' for i in range(n)]
        edge['first_button'] = edge['seats_clockwise'][-1]
        edge['hands'] = 1
        edge['increase_from_hands'] = [1, 2]
        edge['main_increase_from_hand'] = 1
        edge['low']['big_blind_ante'] = 0
        edge['high']['big_blind_ante'] = 0
        edge_expected = reference(edge)
        identical(generator.calculate(edge), edge_expected, 'One-hand/no-ante boundary mismatch')
        require(all(r['big_blind_ante'] == 0 for r in edge_expected['rows']), 'No-ante control failed')
        custom_count += 1

    bad = []
    for field, value in [('seats_clockwise', ['A', 'B']), ('seats_clockwise', ['A', 'A', 'B']),
                         ('first_button', 'missing'), ('hands', True), ('hands', 0),
                         ('hands', 101), ('increase_from_hands', [True]),
                         ('increase_from_hands', [0]), ('increase_from_hands', [8]),
                         ('increase_from_hands', [3, 3]), ('main_increase_from_hand', 8)]:
        item = deepcopy(config)
        item[field] = value
        bad.append(item)
    for level, field, value in [('low', 'small_blind', 0), ('low', 'big_blind', 499),
                                ('low', 'big_blind_ante', -1), ('high', 'small_blind', 499),
                                ('high', 'big_blind_ante', True), ('high', 'big_blind', 10**9 + 1)]:
        item = deepcopy(config)
        item[level][field] = value
        bad.append(item)
    for number, item in enumerate(bad, 1):
        rejects(lambda c=item: reference(c), f'reference bad input {number}')
        rejects(lambda c=item: generator.calculate(c), f'generator bad input {number}')
    for field, value in [('schema_version', 2), ('schema_version', True),
                         ('main_increase_from_hand', True), ('description', '')]:
        item = deepcopy(config)
        item[field] = value
        rejects(lambda c=item: reference(c), 'reference schema: ' + field)

    tamper_count = 0
    for field in ('post', 'cumulative', 'big_blind_ante', 'role', 'button', 'level'):
        altered = deepcopy(result)
        old = altered['rows'][0][field]
        altered['rows'][0][field] = old + 1 if type(old) is int else 'tampered'
        rejects(lambda r=altered: verify_outputs(config, r, posts, summary), 'JSON tamper: ' + field)
        tamper_count += 1
    altered = deepcopy(result)
    altered['summary'][0]['all_hand_posts'] += 1
    rejects(lambda: verify_outputs(config, altered, posts, summary), 'Summary tamper')
    rejects(lambda: verify_outputs(config, result, posts + b'\n', summary), 'Posts CSV tamper')
    rejects(lambda: verify_outputs(config, result, posts, summary + b'\n'), 'Summary CSV tamper')
    tamper_count += 3
    print(json.dumps({'status': 'PASS', 'published_rows': 252, 'published_summaries': 42,
                      'boundaries': 7, 'additional_generator_cases': custom_count,
                      'core_bad_inputs_rejected_by_both': len(bad),
                      'extra_schema_rejections': 4, 'tamper_rejections': tamper_count,
                      'method': 'rotating queue; literal oracles; scale/rotation controls'}, indent=2))


if __name__ == '__main__':
    main()
