"""Independent exact checks for T652, signed Monster Fourier coefficients.

    sage -python checks.py                 # generated values
    sage -python checks.py --stored        # authenticated API readback
Requires the attached generator and JSON files. Companion JSON documents
T79.json, T80.json, T81.json and T635.json may be supplied beside this script;
otherwise they are read through the public API. No writes are performed.
The checker uses published coefficients, independent eta products, character
sums and held-out replication relations, not the generating recurrence.
"""
import numberdb.sage as numberdb
from sage.rings.integer_ring import ZZ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
from pathlib import Path
from math import gcd
import json
import os
import sys
import copy
import generate
from check import exactness, measure, agrees_with, names_its_rings

HERE = Path(__file__).resolve().parent
R = PolynomialRing(ZZ,'X')
X = R.gen()


def document(tid):
    path = HERE / (tid+'.json')
    return json.loads(path.read_text()) if path.exists() else numberdb.table(tid)


def records(doc):
    block = doc['Numbers']
    if isinstance(block,list):
        return [(tuple(str(row['params'][p]) for p in doc.get('Parameters',{})),
                 ZZ(row['number']) if doc['Data properties']['type']=='Z'
                 else row['number']) for row in block if 'number' in row]
    result=[]
    def walk(node, keys):
        if isinstance(node,dict) and 'number' in node:
            result.append((tuple(keys), node['number']))
        elif isinstance(node,dict) and 'equals' in node:
            return
        elif isinstance(node,dict):
            for k,v in node.items():walk(v,keys+[str(k)])
        else:
            result.append((tuple(keys),node))
    walk(block,[])
    return result


def finite_eta(p, nmax):
    """R_p=q^-1 A; A=prod over p∤m of (1-q^m)^(24/(p-1))."""
    exponent,remainder=divmod(24,p-1)
    assert remainder==0
    size=nmax+1
    a=[ZZ(1)]+[ZZ(0)]*size
    inv=[ZZ(1)]+[ZZ(0)]*size
    for m in range(1,size+1):
        if m%p==0:
            continue
        for _ in range(exponent):
            for j in range(size,m-1,-1):a[j]-=a[j-m]
            for j in range(m,size+1):inv[j]+=inv[j-m]
    assert a[1]==-exponent
    factor=p**(12//(p-1))
    return {'B':{n:a[n+1] for n in range(1,nmax+1)},
            'A':{n:a[n+1]+factor*inv[n-1] for n in range(1,nmax+1)}}


def multiply(a,b,cap=30):
    result={}
    for i,x in a.items():
        for j,y in b.items():
            if i+j<=cap:result[i+j]=result.get(i+j,ZZ(0))+x*y
    return {i:x for i,x in result.items() if x}


def laurent_faber(a, max_degree):
    """Build by triangular Laurent cancellation, no Faber recurrence."""
    series={-1:ZZ(1),**a}
    powers=[{0:ZZ(1)}]
    polys={0:R(1)}
    evaluations={0:powers[0]}
    for m in range(1,max_degree+1):
        powers.append(multiply(powers[-1],series))
        q=dict(powers[m]); polynomial=X**m
        for j in range(m-1,-1,-1):
            coefficient=q.get(-j,ZZ(0))
            polynomial-=coefficient*polys[j]
            for k,v in evaluations[j].items():q[k]=q.get(k,ZZ(0))-coefficient*v
        assert q[-m]==1 and all(q.get(k,0)==0 for k in range(-m+1,1))
        polys[m]=polynomial
        evaluations[m]={i:v for i,v in q.items() if v}
    return polys,evaluations


def replication_errors(data):
    errors=[]; compared=0
    for g in generate.CLASSES:
        pol,ev=laurent_faber(data[g],5)
        assert pol[2]==X**2-2*data[g][1]
        assert pol[3]==X**3-3*data[g][1]*X-3*data[g][2]
        for m in [3,5]:
            other=generate.CLASS_DATA['power_maps'][str(m)][g]
            for n in range(1,30//m+1):
                expected=m*data[g][m*n]
                if n%m==0:expected+=data[other][n//m]
                compared+=1
                if ev[m].get(n,0)!=expected:errors.append([g,m,n])
    return errors,compared


def run(stored=False):
    generated=generate.coefficients()
    data=copy.deepcopy(generated)
    if stored:
        doc=document('T652')
        rows=records(doc)
        expected=[(str(p['g']),str(p['n'])) for p in generate.McKayThompsonCoefficients().enumerate()]
        assert [k for k,v in rows]==expected
        data={g:{} for g in generate.CLASSES}
        data['1A']=generated['1A']
        for (g,n),v in rows:data[g][int(n)]=ZZ(v)
    values={(g,n):v for g in generate.CLASSES for n,v in data[g].items()}
    assert not exactness(values)
    assert not names_its_rings(str(HERE/'generate.py'))
    # Known-answer controls execute before comparisons.
    e2=finite_eta(2,30)
    assert (e2['A'][1],e2['A'][2],e2['B'][1],e2['B'][2])==(4372,96256,276,-2048)
    pol,ev=laurent_faber({n:ZZ(0) for n in range(1,31)},5)
    assert pol[5]==X**5 and ev[5]=={-5:1}
    pol,_=laurent_faber(data['2A'],3)
    assert pol[2]==X**2-8744 and pol[3]==X**3-13116*X-288768
    # Negative controls make sure the tests reject wrong signs and shifts.
    wrong=copy.deepcopy(data);wrong['2B'][30]=-wrong['2B'][30]
    assert wrong['2B'][30]!=finite_eta(2,30)['B'][30]
    wrong=copy.deepcopy(data);wrong['6A'][30]+=1
    assert replication_errors(wrong)[0]
    shifted=copy.deepcopy(data);shifted['3A']={n:data['3A'].get(n+1,0) for n in range(1,31)}
    assert replication_errors(shifted)[0]
    # Published first-ten control rows, including all four seeds per class.
    controls=json.loads((HERE/'published-controls.json').read_text())['coefficients']
    for g,expected in controls.items():
        for n,v in enumerate(expected,1):assert data[g][n]==v,(g,n,data[g][n],v)
    # Independent character decompositions in Conway–Norton Table 1a.
    decomposition={1:[1,1],2:[1,1,1],3:[2,2,1,1],4:[2,3,2,1,0,1],5:[4,5,3,2,1,1,1]}
    chars=generate.CLASS_DATA['first15_characters']
    assert chars['1A'][:7]==[1,196883,21296876,842609326,18538750076,19360062527,293553734298]
    for g in data:
        for n,multiplicities in decomposition.items():
            assert sum(a*b for a,b in zip(multiplicities,chars[g]))==data[g][n],(g,n,'character')
    eta_values={}
    for p in [2,3,5,7]:
        for suffix,coefs in finite_eta(p,30).items():
            for n,v in coefs.items():eta_values[(str(p)+suffix,n)]=v
    assert not agrees_with({k:values[k] for k in eta_values},lambda key:eta_values[key])
    errors,replication_count=replication_errors(data)
    assert not errors,errors
    # All 30 identity coefficients agree with the separate existing expansion.
    jrows={int(k[0]):ZZ(v) for k,v in records(document('T79'))}
    for n in range(1,31):assert data['1A'][n]==jrows[n]
    # Formal E4 and Delta source controls supporting the identity-series calculation.
    erows={int(k[0]):ZZ(v) for k,v in records(document('T81'))}
    drows={int(k[0]):ZZ(v) for k,v in records(document('T80'))}
    delta=[ZZ(1)]+[ZZ(0)]*30
    for m in range(1,31):
        for _ in range(24):
            for j in range(30,m-1,-1):delta[j]-=delta[j-m]
    for n in range(1,31):
        assert erows[n]==240*sum(ZZ(d)**3 for d in range(1,n+1) if n%d==0)
        assert drows[n]==delta[n-1]
    faber_rows=dict(records(document('T635')))
    faber_count=0
    for g in ['1A','2A','2B','3A','3B','5A','5B','7A','7B']:
        pol,_=laurent_faber(data[g],20)
        for m in range(2,21):
            assert pol[m]==R(str(faber_rows[(g,str(m))]).replace('^','**')),(g,m,'Faber')
            faber_count+=1
    # Powers have the expected orders; identity plus chosen classes is closed.
    orders=dict(zip(generate.CLASS_DATA['class_names'],generate.CLASS_DATA['class_orders']))
    for p,pm in generate.CLASS_DATA['power_maps'].items():
        assert set(pm)==set(data)
        for g,h in pm.items():assert orders[h]==orders[g]//gcd(orders[g],int(p))
    for g in generate.CLASSES:
        for n in range(1,31):assert abs(data[g][n])<=data['1A'][n]
    report={'table':'T652','mode':'stored' if stored else 'generated',
      'skill_manifest':'agents/runs/20261101T140515Z-build-skills/manifest.json',
      'skill_sha256':'a2b28d9953264d7b0ca62663785c2952190c5be6d6d3597fd695d9a54d606278',
      'all_checks_passed':True,'published_controls':330,'character_decompositions':165,
      'independent_eta_coefficients':240,'held_out_replication_coefficients':replication_count,
      'faber_companions':faber_count,'j_companions':30,'E4_companions':30,'Delta_companions':30,
      'known_answer_controls':True,'wrong_sign_and_offset_controls_rejected':True,
      'negative_entries':sum(v<0 for v in values.values()),'zero_entries':sum(v==0 for v in values.values()),
      'nonzero_entries':sum(v!=0 for v in values.values()),'measurement':measure(values)}
    return report


if __name__=='__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        os.environ['NUMBERDB_API_KEY']=sys.stdin.read().strip()
    print(json.dumps(run('--stored' in sys.argv or os.environ.get('CHECK_STORED')=='1'),indent=2))
