"""Independent count/fraction audit; deliberately does not import report.py."""
import csv
import hashlib
import json
import math
from collections import Counter
from fractions import Fraction as F
from pathlib import Path

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


def main():
    manifest = json.loads((ROOT/'input/manifest.json').read_text())
    source_bytes = (ROOT/'input/source_summary.json').read_bytes()
    assert hashlib.sha256(source_bytes).hexdigest() == manifest['source_sha256']
    source = json.loads(source_bytes)
    assert source['configuration'] == manifest['configuration']
    assert hashlib.sha256(json.dumps(source['configuration'], sort_keys=True).encode()).hexdigest() == manifest['protocol_sha256']
    raw = {}
    for run in source['runs']:
        for budget in run['budgets']:
            for method in manifest['methods']:
                r = budget['attacks'][method]
                v = F(2048-r['failures'], 2048)
                assert float(v) == r['robust_accuracy']
                raw[run['seed'], budget['epsilon'], method] = v
    rows = [json.loads(s) for s in (ROOT/'input/runs.jsonl').read_text().splitlines()]
    assert len(rows) == len(raw) == 60
    assert len({(r['seed'], r['epsilon'], r['method']) for r in rows}) == 60
    for r in rows:
        assert float(raw[r['seed'], r['epsilon'], r['method']]) == r['value']
    report = json.loads((ROOT/'results/report.json').read_text())
    errors = []
    for row in report['groups']:
        values = [raw[s, row['epsilon'], row['method']] for s in (79, 80, 81)]
        mean = sum(values)/3
        sd = math.sqrt(float(sum((v-mean)**2 for v in values)/2))
        errors += [abs(float(mean)-row['mean']), abs(sd-row['sd'])]
    # Multinomial count triples instead of report.py's ordered Cartesian indices.
    delta = [raw[s, .2, 'pgd2']-raw[s, .2, 'pgd20'] for s in (79, 80, 81)]
    mass = Counter()
    for a in range(4):
        for b in range(4-a):
            c = 3-a-b
            ways = math.factorial(3)//(math.factorial(a)*math.factorial(b)*math.factorial(c))
            mass[(a*delta[0]+b*delta[1]+c*delta[2])/3] += ways
    assert sum(mass.values()) == 27
    def percentile(p):
        cumulative = 0
        for value, weight in sorted(mass.items()):
            cumulative += weight
            if F(cumulative, 27) >= p:
                return float(value)
        raise AssertionError('no quantile')
    bounds = [percentile(F(1, 40)), percentile(F(39, 40))]
    pair = report['paired_pgd2_minus_pgd20']
    errors += [abs(float(sum(delta)/3)-pair['mean'])]
    errors += [abs(a-b) for a, b in zip(bounds, pair['percentile95'])]
    samples = json.loads((ROOT/'results/bootstrap_exact.json').read_text())
    expected = sorted(float(v) for v, w in mass.items() for _ in range(w))
    assert sorted(samples) == expected
    csv_rows = list(csv.DictReader((ROOT/'results/table.csv').open()))
    assert len(csv_rows) == 20
    for row, original in zip(csv_rows, report['groups']):
        for key in ('mean', 'sd'):
            assert float(row[key]) == original[key]
    assert max(errors) < 1e-14
    audit = dict(passed=True, source_count_rows=60, aggregate_groups=20,
                 exact_bootstrap_mass=27, quantile='inverse empirical CDF',
                 paired_bounds=bounds, max_error=max(errors),
                 limitations='Summary replay only; no model retraining or individual-prediction replay')
    (ROOT/'results/audit.json').write_text(json.dumps(audit, indent=2)+'\n')
    print(json.dumps(audit, indent=2))


if __name__ == '__main__':
    main()
