#!/usr/bin/env python3
"""Independently verify this published synthetic ledger, using only stdlib.

Run: python verify.py [directory-containing-the-CSV-and-JSON-files]
The literal fixture oracle detects missing rows in THIS example. It cannot
establish whether a player's real-world receipts are complete.
No generator import and no assert statements (also works with python -O).
"""
import copy
import csv
import hashlib
import io
import json
import re
import sys
from decimal import Decimal, localcontext, ROUND_HALF_UP
from fractions import Fraction
from pathlib import Path

ENTRY_FIELDS = ['event_id', 'entry_id', 'currency', 'contribution_cents', 'fee_cents', 'paid_cents']
EVENT_FIELDS = ['event_id', 'currency', 'settled', 'expected_entries', 'prize_cents']
# Independently transcribed, fixed examples printed in the article.
ENTRY_ORACLE = [
    ['A', 'A1', 'USD', '10000', '1000', '11000'],
    ['A', 'A2', 'USD', '10000', '1000', '11000'],
    ['A', 'A3', 'USD', '10000', '1000', '11000'],
    ['B', 'B1', 'USD', '5000', '500', '5500'],
    ['C', 'C1', 'USD', '20000', '2000', '22000'],
    ['C', 'C2', 'USD', '20000', '2000', '22000'],
]
EVENT_ORACLE = [
    ['A', 'USD', 'true', '3', '25000'],
    ['B', 'USD', 'true', '1', '0'],
    ['C', 'USD', 'true', '2', '80000'],
]
SCOPE = 'Completed self-funded cash-entry non-bounty events; no refunds, tickets, swaps, staking, rebuys, add-ons, expenses, tax or FX.'


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


def csv_rows(text, fields):
    reader = csv.DictReader(io.StringIO(text))
    require(reader.fieldnames == fields, 'Unexpected or duplicated CSV column names')
    rows = list(reader)
    require(rows and all(set(r) == set(fields) and all(v is not None for v in r.values()) for r in rows),
            'Empty CSV, extra cell or missing cell')
    return rows


def integer(value, field):
    require(isinstance(value, str) and re.fullmatch(r'0|[1-9][0-9]*', value) is not None,
            field + ' must be an explicit nonnegative integer')
    return int(value)


def validate_rows(entries, events, use_oracle=True):
    event_ids = [r['event_id'] for r in events]
    entry_ids = [r['entry_id'] for r in entries]
    require(all(event_ids) and len(set(event_ids)) == len(event_ids), 'Duplicate/empty event ID: one settled prize record per event')
    require(all(entry_ids) and len(set(entry_ids)) == len(entry_ids), 'Duplicate/empty entry ID')
    for row in events:
        require(row['currency'] == 'USD', 'Event currency mismatch')
        require(row['settled'] == 'true', 'Unsettled event cannot be treated as a zero or final prize')
        require(integer(row['expected_entries'], 'expected_entries') > 0, 'Expected count must be positive')
        integer(row['prize_cents'], 'prize_cents')
    for row in entries:
        require(row['event_id'] in event_ids, 'Entry references a missing event')
        require(row['currency'] == 'USD', 'Entry currency mismatch')
        contribution = integer(row['contribution_cents'], 'contribution_cents')
        fee = integer(row['fee_cents'], 'fee_cents')
        paid = integer(row['paid_cents'], 'paid_cents')
        require(paid > 0, 'Every entry requires a known positive cost')
        require(contribution + fee == paid, 'Contribution plus fee must equal paid amount')
    for row in events:
        count = sum(r['event_id'] == row['event_id'] for r in entries)
        require(count == integer(row['expected_entries'], 'expected_entries'), 'Entry count does not reconcile')
    if use_oracle:
        require(sorted([r[f] for f in ENTRY_FIELDS] for r in entries) == ENTRY_ORACLE,
                'Literal six-entry fixture mismatch; this is not proof of real-world completeness')
        require(sorted([r[f] for f in EVENT_FIELDS] for r in events) == EVENT_ORACLE,
                'Literal three-event fixture mismatch')


def ratio(value):
    with localcontext() as context:
        context.prec = 80
        pct = (Decimal(value.numerator) * 100 / Decimal(value.denominator)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    return {'numerator': value.numerator, 'denominator': value.denominator, 'percent_display': format(pct, '.2f')}


def calculate(entries, events):
    out = []
    for event in sorted(events, key=lambda r: r['event_id']):
        matching = [r for r in entries if r['event_id'] == event['event_id']]
        paid = sum(int(r['paid_cents']) for r in matching)
        prize = int(event['prize_cents'])
        out.append({'event_id': event['event_id'], 'entry_count': len(matching), 'paid_cents': paid,
                    'prize_cents': prize, 'net_cents': prize - paid, 'roi': ratio(Fraction(prize - paid, paid))})
    paid = sum(r['paid_cents'] for r in out)
    prize = sum(r['prize_cents'] for r in out)
    net = prize - paid
    mean = sum((Fraction(r['net_cents'], r['paid_cents']) for r in out), Fraction()) / len(out)
    sensitivity = []
    for count in (1, 2, 3):
        cost = count * 11000
        sensitivity.append({'entry_count': count, 'paid_cents': cost, 'prize_cents': 25000,
                            'net_cents': 25000 - cost, 'roi': ratio(Fraction(25000 - cost, cost))})
    require((paid, prize, net) == (82500, 105000, 22500), 'Literal total oracle failed')
    require(Fraction(net, paid) == Fraction(3, 11), 'Literal pooled ROI oracle failed')
    require(mean == Fraction(-14, 99), 'Literal event-mean oracle failed')
    return {'schema_version': 1, 'synthetic': True, 'currency': 'USD', 'scope': SCOPE,
            'events': out,
            'total': {'event_count': len(out), 'entry_count': len(entries), 'paid_cents': paid,
                      'prize_cents': prize, 'net_cents': net, 'roi': ratio(Fraction(net, paid))},
            'mean_event_roi': ratio(mean), 'unknown_A_entry_count_sensitivity': sensitivity}


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


def same_json(actual, expected):
    # JSON string equality additionally distinguishes true/1 and 1.0/1.
    require(json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True),
            'Published result does not match independent exact calculation')


def rejection_controls(entries, events, expected):
    controls = []
    def reject(name, change, oracle=True):
        e, v = copy.deepcopy(entries), copy.deepcopy(events)
        change(e, v)
        try:
            validate_rows(e, v, oracle)
        except ValueError:
            controls.append(name)
        else:
            raise ValueError('Rejection control unexpectedly accepted: ' + name)
    reject('duplicate entry ID', lambda e, v: e[1].update(entry_id='A1'))
    reject('duplicate prize/event record', lambda e, v: v.append(dict(v[0])))
    reject('missing event reference', lambda e, v: e[0].update(event_id='Z'))
    reject('entry currency mismatch', lambda e, v: e[0].update(currency='EUR'))
    reject('event currency mismatch', lambda e, v: v[0].update(currency='EUR'))
    reject('unsettled prize', lambda e, v: v[0].update(settled='false'))
    reject('unknown prize', lambda e, v: v[0].update(prize_cents=''))
    reject('negative prize', lambda e, v: v[0].update(prize_cents='-1'))
    reject('unknown paid cost', lambda e, v: e[0].update(paid_cents=''))
    reject('zero paid cost', lambda e, v: e[0].update(contribution_cents='0', fee_cents='0', paid_cents='0'))
    reject('negative fee', lambda e, v: e[0].update(fee_cents='-1'))
    reject('inexact split', lambda e, v: e[0].update(fee_cents='999'))
    reject('fractional cents', lambda e, v: e[0].update(paid_cents='11000.5'))
    reject('missing entry with original count', lambda e, v: e.pop(0))
    def hide_entry(e, v):
        e.pop(0)
        v[0]['expected_entries'] = '2'
    # Demonstrate explicitly why counts alone cannot prove completeness.
    e, v = copy.deepcopy(entries), copy.deepcopy(events)
    hide_entry(e, v)
    validate_rows(e, v, use_oracle=False)
    reject('missing entry plus altered count rejected by fixed fixture oracle', hide_entry)
    altered = copy.deepcopy(expected)
    altered['total']['roi']['denominator'] = 22
    try:
        same_json(altered, expected)
    except ValueError:
        controls.append('tampered published ratio')
    else:
        raise ValueError('Tampered result control unexpectedly accepted')
    return controls


def verify_manifest(directory):
    path = directory / 'SHA256SUMS'
    if not path.exists():
        return 'absent (optional during development)'
    count, seen = 0, set()
    for line in path.read_text(encoding='utf-8-sig').splitlines():
        if not line.strip():
            continue
        match = re.fullmatch(r'([0-9a-fA-F]{64})\s+\*?(.+)', line)
        require(match is not None, 'Malformed manifest line')
        digest, name = match.groups()
        require(name == Path(name).name and name not in ('.', '..') and '\\' not in name and '/' not in name,
                'Manifest file must be a local basename')
        require(name not in seen, 'Duplicate manifest file')
        seen.add(name)
        target = directory / name
        require(target.is_file(), 'Manifest file missing: ' + name)
        require(hashlib.sha256(target.read_bytes()).hexdigest() == digest.lower(), 'Manifest digest mismatch: ' + name)
        count += 1
    require({'entries.csv', 'events.csv', 'results.json', 'verify.py'} <= seen, 'Manifest omits a core fixture/verifier file')
    return str(count) + ' file hashes checked'


def main():
    require(len(sys.argv) <= 2, 'Usage: python verify.py [fixture-directory]')
    directory = Path(sys.argv[1]).resolve() if len(sys.argv) == 2 else Path(__file__).resolve().parent
    entries = csv_rows((directory / 'entries.csv').read_text(encoding='utf-8-sig'), ENTRY_FIELDS)
    events = csv_rows((directory / 'events.csv').read_text(encoding='utf-8-sig'), EVENT_FIELDS)
    validate_rows(entries, events)
    expected = calculate(entries, events)
    actual = json.loads((directory / 'results.json').read_text(encoding='utf-8-sig'), object_pairs_hook=json_pairs)
    same_json(actual, expected)
    controls = rejection_controls(entries, events, expected)
    print(json.dumps({'status': 'PASS', 'events': 3, 'entries': 6, 'exact_values': True,
                      'rejection_controls': controls, 'manifest': verify_manifest(directory),
                      'limitation': 'Validates a fixed synthetic fixture, not real-world receipt completeness.'}, indent=2))


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