"""Independent checks for psi-class intersection numbers -- numberdb.org/T458.

Run: sage -python checks.py
Check a saved API document: sage -python checks.py stored-document.json
Read the draft and check its values: sage -python checks.py --stored
(The last command requires its owner's NUMBERDB_API_KEY.)

Liu-Xu, arXiv:0710.5322, Theorem 1.1, computes by genus. It uses
Fraction arithmetic and no generator arithmetic. It starts with the
multinomial genus-zero formula and derives <tau_1>_1 rather than using
that number as an input. Source controls are the Macaulay2 HodgeIntegrals
wittenTau documentation, including <tau_5^3>_5 = 41873/255467520.

check.py is the unchanged NumberDB table-build validation helper bundled
for reproducibility. This file prints a JSON evidence report and writes
nothing to the site.
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from fractions import Fraction
from functools import lru_cache
from itertools import combinations
from math import factorial, prod
from pathlib import Path
import json
import os
import sys
import time


@lru_cache(None)
def liu_xu(g, ds):
    """Independent genus recursion on an immutable sorted profile."""
    n = len(ds)
    if g < 0 or any(d < 0 for d in ds) or 2 * g - 2 + n <= 0:
        return Fraction(0)
    if sum(ds) != 3 * g - 3 + n:
        return Fraction(0)
    if g == 0:
        return Fraction(factorial(n - 3), prod(factorial(d) for d in ds))
    if 0 in ds:
        reduced = list(ds)
        reduced.remove(0)
        result = Fraction(0)
        for i, degree in enumerate(reduced):
            if degree:
                other = reduced.copy()
                other[i] -= 1
                result += liu_xu(g, tuple(sorted(other)))
        return result
    # The theorem reduces genus when every exponent is positive. Its factors
    # with genus 0 vanish by dimension, so only h=1,...,g-1 can contribute.
    distinguished = ds[-1]
    remaining = ds[:-1]
    c = 2 * g + n - 1
    term = Fraction(2 * distinguished + 3, 12) * liu_xu(g - 1, tuple(sorted((0,0,0,0,distinguished + 1) + remaining)))
    term -= Fraction(c, 6) * liu_xu(g - 1, (0,0,0) + ds)
    for size in range(len(remaining) + 1):
        for subset in combinations(range(len(remaining)), size):
            I = tuple(remaining[i] for i in subset)
            J = tuple(remaining[i] for i in range(len(remaining)) if i not in subset)
            for h in range(1, g):
                right = liu_xu(g - h, (0,0) + J)
                if right:
                    left1 = liu_xu(h, tuple(sorted((0,0,distinguished + 1) + I)))
                    left2 = liu_xu(h, tuple(sorted((0,distinguished) + I)))
                    term += ((2 * distinguished + 3) * left1 - c * left2) * right
    return term / (c * (c - 1))


def expected(key):
    g, n, ds = key
    assert n == len(ds)
    x = liu_xu(g, ds)
    return QQ(x.numerator) / QQ(x.denominator)


def from_document(doc):
    """The API returns nested Numbers keyed by g, n, and the profile string."""
    result = {}
    for gs, by_n in doc['Numbers'].items():
        for ns, by_d in by_n.items():
            for profile, entry in by_d.items():
                value = entry['number'] if isinstance(entry, dict) else entry
                key = (int(gs), int(ns), tuple(map(int, profile.split(','))))
                assert key not in result
                rational = Fraction(str(value))
                result[key] = QQ(rational.numerator) / QQ(rational.denominator)
    return result


def check_all(values):
    from check import exactness, agrees_with, measure, names_its_rings
    from generate import MAX_GENUS, MAX_MARKS
    assert not exactness(values)
    assert not names_its_rings(str(Path(__file__).with_name('generate.py')))
    # Controls before comparisons: a failed normalisation must stop the run.
    controls = {
        (0,(0,0,0)): Fraction(1),
        (0,(0,0,0,0,1,2)): Fraction(3),
        (0,(0,0,0,0,0,2,2)): Fraction(6),
        (1,(1,)): Fraction(1,24),
        (2,(4,)): Fraction(1,1152),
        (2,(2,3)): Fraction(29,5760),
        (5,(5,5,5)): Fraction(41873,255467520),
    }
    for (g,ds), value in controls.items():
        assert liu_xu(g,ds) == value, (g,ds,'independent control')
        if (g,len(ds),ds) in values:
            assert values[g,len(ds),ds] == QQ(value.numerator) / QQ(value.denominator), (g,ds,'stored control')
    disagreements = agrees_with(values, expected)
    assert not disagreements, disagreements[:8]
    counts = {'independent_Liu_Xu':len(values),'known_answer_controls':len(controls),
              'genus_zero':0,'one_point':0,'string':0,'dilaton':0,
              'dimension_and_sorted_profile':0}
    # Independent completeness count using combinations_with_replacement,
    # not the recursive profile enumerator that fills the table.
    from itertools import combinations_with_replacement
    domain=set()
    for g in range(MAX_GENUS+1):
        for n in range(1,MAX_MARKS+1):
            if 2*g-2+n<=0:
                continue
            total=3*g-3+n
            for ds in combinations_with_replacement(range(total+1),n):
                if sum(ds)==total:
                    domain.add((g,n,ds))
    assert set(values)==domain, (len(values),len(domain))
    for (g,n,ds), value in values.items():
        assert g >= 0 and 2*g-2+n>0 and n==len(ds)
        assert tuple(sorted(ds))==ds and min(ds)>=0 and sum(ds)==3*g-3+n
        assert value>0
        counts['dimension_and_sorted_profile']+=1
        if g==0:
            assert value==QQ(factorial(n-3)) / QQ(prod(factorial(d) for d in ds))
            counts['genus_zero']+=1
        if n==1:
            assert value==QQ(1) / QQ(24**g*factorial(g))
            counts['one_point']+=1
        if 0 in ds and (g,n)!=(0,3):
            rest=list(ds);rest.remove(0)
            rhs=QQ(0)
            for j,d in enumerate(rest):
                if d:
                    reduced=rest.copy();reduced[j]-=1
                    rhs+=values[g,n-1,tuple(sorted(reduced))]
            assert value==rhs
            counts['string']+=1
        if 1 in ds and (g,n)!=(1,1):
            rest=list(ds);rest.remove(1)
            assert value==(2*g-3+n)*values[g,n-1,tuple(rest)]
            counts['dilaton']+=1
    return {'ok':True,'checks':counts,'measurement':measure(values),
            'independent_cache':str(liu_xu.cache_info())}


if __name__=='__main__':
    start=time.monotonic()
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        numberdb.configure(api_key=sys.stdin.read().strip())
    if sys.argv[1:]==['--stored']:
        values=from_document(numberdb.table('T458'))
    elif sys.argv[1:]:
        values=from_document(json.loads(Path(sys.argv[1]).read_text()))
    else:
        from generate import PsiIntersections
        gen=PsiIntersections()
        values={(int(p['g']),int(p['n']),tuple(map(int,p['d'].split(',')))):
                gen.value(p,100) for p in gen.enumerate()}
    report=check_all(values)
    report['elapsed_seconds']=time.monotonic()-start
    print(json.dumps(report,indent=2,default=str))
