"""Checks for Universal amplitude ratios -- numberdb.org/T476.

    sage -python checks.py
    sage -python checks.py --stored  # key needed while T476 is private

This does not recompute Monte Carlo estimates. It checks exact transcription,
quoted uncertainty conversion, algebraic consistency, and comparisons with
independent published determinations. A three-combined-error diagnostic is
not a confidence-level claim: the quoted errors also include systematics.
"""

import json
import os
from pathlib import Path
import re
import sys
from fractions import Fraction

import numberdb.sage as numberdb
from generate import IsingAmplitudeRatios, client_from_environment
from check import exactness, measure, agrees_with, names_its_rings

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


def rational(s):
    # Fraction converts decimal text exactly; no binary float or Sage decimal parser.
    v = Fraction(s)
    return Fraction(v.numerator) / Fraction(v.denominator)


def parentheses(s):
    m = re.fullmatch(r'(\d+\.\d+)\((\d+)\)', s)
    assert m, s
    n = len(m[1].split('.')[1])
    return rational(m[1]), Fraction(int(m[2])) / Fraction(10)**n


def centre_radius(s):
    centre, radius = s.split(' +/- ')
    return rational(centre), rational(radius)


def contains(pair, x):
    return abs(x - pair[0]) <= pair[1]


def identity_witness(pairs):
    # An exact common point inside all eight boxes, not just separate overlaps.
    point = {key: pair[0] for key, pair in pairs.items()}
    point['Q-plus'] = point['Q-minus'] * point['U0'] * point['Uxi']**3
    point['u-star'] = 3 * point['Uxi']**3 / (point['U2'] * point['Qc'])
    assert all(contains(pairs[k], v) for k, v in point.items())
    assert point['Q-plus'] / point['Q-minus'] == point['U0'] * point['Uxi']**3
    assert point['u-star'] * point['Qc'] == 3 * point['Uxi']**3 / point['U2']
    return point


def ratios(a_plus, a_minus, c_plus, c_minus, f_plus, f_minus, f_exp_minus, b, alpha):
    return (a_plus/a_minus, c_plus/c_minus, f_plus/f_minus,
            f_exp_minus/f_minus, alpha*a_plus*f_plus**3,
            alpha*a_minus*f_minus**3, 3*c_minus/(f_minus**3*b**2),
            f_plus**3*b**2/c_plus)


def controls():
    assert parentheses('0.01899(10)') == (Fraction(1899)/100000, Fraction(1)/10000)
    assert centre_radius('14.08 +/- 0.05') == (Fraction(352)/25, Fraction(1)/20)
    assert contains((Fraction(2), Fraction(1)/10), Fraction(21)/10)
    assert not contains((Fraction(2), Fraction(1)/10), Fraction(211)/100)
    # A known exact rational toy set satisfying both identities.
    p = {k: (Fraction(v), Fraction(0)) for k, v in {
        'U0': 2, 'U2': 3, 'Uxi': 2, 'Qxi-minus': 1,
        'Q-minus': 1, 'Q-plus': 16, 'Qc': 4, 'u-star': 2}.items()}
    identity_witness(p)
    p['Q-plus'] = (Fraction(17), Fraction(0))
    try:
        identity_witness(p)
    except AssertionError:
        pass
    else:
        raise AssertionError('The identity check accepted a known wrong value')
    # Thermal, field and length metric changes. Exponents satisfy scaling:
    # alpha=1/8, nu=5/8, beta=5/16, gamma=5/4. Let t'=65536t, h'=3h,
    # and x'=x/2. Reduced free-energy density gains the volume factor 2^3.
    ap, am, cp, cm, fp, fm, fe, b = map(Fraction, (2, 4, 3, 5, 4, 2, 3, 5))
    base = ratios(ap, am, cp, cm, fp, fm, fe, b, Fraction(1)/8)
    assert base[0] == Fraction(1)/2 and base[4] == 16 and base[5] == 4
    transformed = ratios(
        ap*8/2**30, am*8/2**30, cp*8*2**20/9, cm*8*2**20/9,
        fp*2**10/2, fm*2**10/2, fe*2**10/2, b*8/(3*2**5), Fraction(1)/8)
    assert transformed == base
    return 'parenthetical-error, containment, exact identity, rejecting bad identity, and metric-invariance controls passed'


def run_checks(values):
    result = {'controls': controls(), 'entries': len(values)}
    assert len(values) == 8
    assert not names_its_rings(str(HERE/'generate.py'))
    assert not exactness(values)
    result['measure'] = measure(values)
    data = json.loads((HERE/'source-data.json').read_text())
    pairs = {key: centre_radius(val['number']) for key, val in values.items()}
    expected = data['primary']['parentheses']
    assert set(pairs) == set(expected)
    assert not agrees_with(pairs, lambda key: parentheses(expected[key]))
    assert all(c > r > 0 and r/c < Fraction(1)/100 for c, r in pairs.values())
    result['source_transcription'] = 'all eight centres and radii match the printed parenthetical uncertainties exactly'
    result['joint_identity_witness'] = {k: str(v) for k, v in identity_witness(pairs).items()}
    # Direct mutation test on this data, not only the toy set.
    wrong = dict(pairs)
    wrong['Q-plus'] = (pairs['Q-plus'][0]*10, pairs['Q-plus'][1])
    try:
        identity_witness(wrong)
    except AssertionError:
        result['bad_data_control'] = 'a tenfold error in Q-plus is rejected'
    else:
        raise AssertionError('Failed to reject a tenfold error')
    comparisons = []
    for source, group in data['independent'].items():
        for key, written in group['parentheses'].items():
            x, dx = pairs[key]
            y, dy = parentheses(written)
            delta = abs(x-y)
            # No floating arithmetic is involved in accepting the diagnostic.
            squared = delta**2/(dx**2+dy**2)
            assert squared < 9, (key, source, squared)
            comparisons.append({
                'ratio': key, 'source': source, 'independent_quote': written,
                'quoted_intervals_overlap': bool(delta <= dx+dy),
                'difference_in_combined_errors_squared': str(squared),
                'less_than_three_combined_errors': True})
    assert {c['ratio'] for c in comparisons} == set(pairs)
    result['independent_comparisons'] = comparisons
    result['comparison_limit'] = ('These are separate measured determinations, not exact computations. '
        'Non-overlap is retained as a discrepancy; the cause of the statistical/systematic shifts '
        'is not independently determined here. No uncertainty is enlarged and no mean is taken.')
    result['environment'] = IsingAmplitudeRatios().environment()
    return result


if __name__ == '__main__':
    g = IsingAmplitudeRatios()
    if '--stored' in sys.argv or os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        client = client_from_environment()
        doc = numberdb.table(g.table, client=client)
        raw = doc['Numbers']
        values = (raw if isinstance(raw, dict) else
                  {entry['params']['ratio']: entry for entry in raw})
        report = run_checks(values)
        report['read_back'] = True
        verification = g.verify(sample=None, client=client)
        assert verification.ok, str(verification)
        report['generator_verify'] = str(verification)
    else:
        values = {params['ratio']: g.value(params, g.digits) for params in g.enumerate()}
        report = run_checks(values)
    print(json.dumps(report, indent=2))
