"""Independent exact checks for T570 (sage -python checks.py).

The verifier reconstructs every even polynomial by interpolation at 21
half-integer Bessel orders. Its input is the elementary K recurrence, not
the product used in generate.py. K_(n+1/2)(z) = sqrt(pi/(2z))*exp(-z)*P_n(1/z),
where P_0=1, P_1=1+x and P_(n+1)=(2n+1)*x*P_n+P_(n-1).
Differentiating gives (1+x/2)*P_n+x^2*P_n'. Thus the coefficient of x^k
is b_k(n+1/2). Interpolation in t=nu^2 determines the degree <=20 polynomial.

Sources: DLMF 10.29.1 (with the modified cylinder-function convention),
10.39.2 and 10.49.12; NumberDB T116 and T199 fix the Bessel-polynomial
normalization. Local JSON snapshots of T116 and T569, when present, enable
cross-table readback checks. A snapshot T570.json enables stored-value checks.
No website writes occur. Run the same checks on all stored rows after filling.
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
from math import factorial
from pathlib import Path
import json
import platform
import importlib.metadata

from check import exactness, measure, agrees_with, names_its_rings

V = PolynomialRing(QQ, 'v')
v = V.gen()
X = PolynomialRing(QQ, 'x')
x = X.gen()
T = PolynomialRing(QQ, 't')
t = T.gen()
MAX_K = 20


def elementary_polynomials():
    polynomials = [X.one(), 1+x]
    for n in range(1, MAX_K):
        polynomials.append((2*n+1)*x*polynomials[-1]+polynomials[-2])
    return polynomials


def interpolate(nodes, values):
    result = T.zero()
    for i, node in enumerate(nodes):
        basis = T.one()
        denominator = QQ.one()
        for j, other in enumerate(nodes):
            if j != i:
                basis *= t-other
                denominator *= node-other
        result += (QQ(values[i])/denominator)*basis
    return result


def independent_values():
    # Positive controls run before the reconstruction they validate.
    P = elementary_polynomials()
    assert P[:4] == [X(1), 1+x, 1+3*x+3*x**2,
                     1+6*x+15*x**2+15*x**3]
    D = [(1+x/QQ(2))*p+x**2*p.derivative() for p in P]
    assert D[0] == 1+x/QQ(2)
    assert D[1] == 1+QQ(3)/QQ(2)*x+QQ(3)/QQ(2)*x**2
    nodes = [(QQ(n)+QQ(1)/QQ(2))**2 for n in range(MAX_K+1)]
    for power in (0, 1, 2):
        assert interpolate(nodes, [node**power for node in nodes]) == t**power
    reconstructed = {}
    for k in range(MAX_K+1):
        polynomial = interpolate(nodes, [d[k] for d in D])
        assert polynomial.degree() == k
        reconstructed[k] = V(polynomial(v**2))
    return reconstructed, P, D


def check_values(values, reconstructed, P, D):
    assert list(values) == list(range(MAX_K+1)), 'wrong extent or row order'
    assert not exactness(values)
    assert not agrees_with(values, lambda k: reconstructed[k])
    assert values[0] == 1 and values[1] == v**2/QQ(2)+QQ(3)/QQ(8)
    A = [V.one()]
    for k in range(1, MAX_K+1):
        A.append((4*v**2-(2*k-1)**2)*A[-1]/QQ(8*k))
    for k, value in values.items():
        assert value.degree() == 2*k
        assert value(-v) == value
        assert value.leading_coefficient() == QQ(1)/QQ(2**k*factorial(k))
        assert value == (A[k](v-1)+A[k](v+1))/QQ(2)
        if k:
            assert value == A[k]+(QQ(k)-QQ(1)/QQ(2))*A[k-1]
        for j in range(1, k):
            root = QQ(j)-QQ(1)/QQ(2)
            assert value(root) == 0 and value(-root) == 0
        for n in range(MAX_K+1):
            assert value(QQ(n)+QQ(1)/QQ(2)) == D[n][k]
    # These generate the full polynomial in x whenever n+1 <= MAX_K.
    for n in range(MAX_K):
        assert X([values[k](QQ(n)+QQ(1)/QQ(2))
                  for k in range(MAX_K+1)]) == D[n]
    assert [values[k](QQ(1)/QQ(2)) for k in range(MAX_K+1)] == [QQ(1),QQ(1)/QQ(2)]+[QQ(0)]*(MAX_K-1)
    return {'interpolated_polynomials':len(values),
            'half_integer_specializations':len(values)*len(P),
            'half_integer_derivative_polynomial_identities':MAX_K,
            'degree_evenness_leading_coefficient':'pass',
            'half_integer_roots':'pass',
            'two_relations_with_a_k':'pass'}


def snapshot_values(path, ring):
    doc = json.loads(path.read_text())
    records = doc['Numbers']
    if isinstance(records, dict):
        return {int(k):ring(value['number'] if isinstance(value,dict) else value)
                for k,value in records.items()}
    return {int(entry['params']['k']):ring(entry['number']) for entry in records}


def main():
    from generate import HankelDerivative
    directory = Path(__file__).resolve().parent
    reconstructed,P,D = independent_values()
    generator = HankelDerivative()
    assert not names_its_rings(str(directory/'generate.py'))
    values = {int(p['k']):generator.value(p,100) for p in generator.enumerate()}
    result = {'table':'T570','checks':check_values(values,reconstructed,P,D),
              'measurement':measure(values)}
    corrupted = dict(values)
    corrupted[20] += QQ(1)/QQ(10**20)
    try:
        check_values(corrupted,reconstructed,P,D)
    except AssertionError:
        result['deliberate_corruption_rejected'] = True
    else:
        raise AssertionError('the independent verifier accepted corrupted data')
    if (directory/'T116.json').exists():
        stored_P = snapshot_values(directory/'T116.json',X)
        assert all(stored_P[n] == P[n] for n in range(MAX_K+1))
        result['stored_T116_matches'] = MAX_K+1
    if (directory/'T569.json').exists():
        stored_A = snapshot_values(directory/'T569.json',V)
        assert all(values[k] == (stored_A[k](v-1)+stored_A[k](v+1))/QQ(2)
                   for k in range(MAX_K+1))
        assert all(values[k] == stored_A[k]+(QQ(k)-QQ(1)/QQ(2))*stored_A[k-1]
                   for k in range(1,MAX_K+1))
        result['stored_T569_relations'] = {'shift_identity':MAX_K+1,'derivative_identity':MAX_K}
    if (directory/'T570.json').exists():
        stored = snapshot_values(directory/'T570.json',V)
        result['stored_T570_checks'] = check_values(stored,reconstructed,P,D)
        assert stored == values
        document=json.loads((directory/'T570.json').read_text())
        namespace={}
        exec(document['Programs']['program-sage']['code'],namespace)
        assert namespace['values'] == list(stored.values())
        result['stored_program_matches_all_entries'] = True
    result['versions'] = {'python':platform.python_version()}
    for package in ('numberdb','passagemath-standard','passagemath-symbolics'):
        try:result['versions'][package]=importlib.metadata.version(package)
        except importlib.metadata.PackageNotFoundError:pass
    try:
        from sage.version import version
        result['versions']['sage'] = version
    except ImportError:pass
    print(json.dumps(result,indent=2))
    print('VALUES_JSON '+json.dumps({str(k):str(value) for k,value in values.items()}))


if __name__ == '__main__':
    main()
