"""Independent checks for Chowla–Selberg period constants -- numberdb.org/T669.

Run `sage -python checks.py` with generate.py and check.py beside this file.
The core comparison uses no Gamma function: reduced binary quadratic forms
and a finite Dedekind eta product with a proved logarithmic tail bound.
Optional companion JSON exports T230.json, T296.json, T506.json, T177.json,
T129.json and T211.json allow checks against live NumberDB values. They can
be fetched with numberdb.table(TID); no saved numerical value defines a row.
For stored values use main(stored_path='stored-table.json'). The build wrapper
passes the authenticated draft export. This file never writes to the website.

Skill manifest: agents/runs/20261102T045202Z-build-skills/manifest.json.
Both versions: a2b28d9953264d7b0ca62663785c2952190c5be6d6d3597fd695d9a54d606278.
"""
import numberdb.sage as numberdb
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
from sage.rings.real_arb import RealBallField
from sage.rings.real_mpfi import RealIntervalField
from sage.libs.pari import pari
from numberdb._write import to_text
from generate import ChowlaSelbergPeriods, character_data, period
from check import exactness, measure, names_its_rings
from math import gcd, isqrt
from decimal import Decimal
from pathlib import Path
import json
import platform
import sage.version

PRECISION = 512
TERMS = 80
R = RealBallField(PRECISION)
I = RealIntervalField(PRECISION)
HERE = Path(__file__).parent


def reduced_forms(D):
    # A reduced primitive positive definite form satisfies 3*a*a <= |D|.
    out = []
    for a in range(1, isqrt((-D)//3)+1):
        for b in range(-a, a+1):
            if (b*b-D) % (4*a):
                continue
            c = (b*b-D)//(4*a)
            if a > c or gcd(gcd(a, b), c) != 1:
                continue
            if (abs(b) == a or a == c) and b < 0:
                continue
            out.append((a, b, c))
    return out


def squarefree_trial(n):
    return all(n % (p*p) for p in range(2, isqrt(n)+1))


def independent_discriminants(limit):
    # Enumerate squarefree negative radicands rather than testing discriminants.
    fields = []
    for m in range(1, limit+1):
        if squarefree_trial(m):
            D = -m if (-m) % 4 == 1 else -4*m
            if -D <= limit:
                fields.append(D)
    return sorted(fields, key=abs)


def log_F(a, b, D):
    y = R(-D).sqrt()/(2*a)
    theta = -R.pi()*b/a
    r = (-2*R.pi()*y).exp()
    total = y.log()/2 - R.pi()*y/6
    rn = R(1)
    for n in range(1, TERMS+1):
        rn *= r
        total += (1-2*rn*(n*theta).cos()+rn*rn).log()
    tail = 2*r**(TERMS+1)/((1-r)*(1-r**(TERMS+1)))
    assert tail.is_finite() and tail >= 0 and tail < R(10)**(-185)
    return total.add_error(tail), tail


def eta_period(D, forms):
    terms = [log_F(a, b, D) for a,b,c in forms]
    v = ((2*R.pi()).log()+R(-D).log()/4+
         sum((x[0] for x in terms),R(0))/len(forms)).exp()
    return v, max(t for x,t in terms)


def narrow(x, bits=350):
    assert x.is_finite() and x.accuracy() >= bits, str(x)


def agrees(x, y, bits=310):
    narrow(x,bits); narrow(y,bits)
    return bool(x.overlaps(y))


def rational_decimal(s):
    n,d = Decimal(s).as_integer_ratio()
    return QQ(n)/d


def decimal_data(s):
    dec = Decimal(s)
    center = rational_decimal(s)
    unit = QQ(10)**int(dec.as_tuple().exponent)
    return center, unit


def written_ball(s):
    center,unit = decimal_data(s)
    return R(I(center-unit,center+unit))


def contained(x,s):
    center,unit = decimal_data(s)
    interval=I(x)
    lo=interval.lower().exact_rational()
    hi=interval.upper().exact_rational()
    return center-unit <= lo and hi <= center+unit


def num(x):
    return x['number'] if isinstance(x,dict) else x


def optional(tid):
    p=HERE/(tid+'.json')
    return json.loads(p.read_text()) if p.exists() else None


def main(stored_path=None):
    # Known-answer and rejection controls run before any family comparison.
    assert reduced_forms(-3)==[(1,1,1)]
    assert reduced_forms(-4)==[(1,0,1)]
    assert reduced_forms(-23)==[(1,1,6),(2,-1,3),(2,1,3)]
    assert len(reduced_forms(-163))==1
    assert not squarefree_trial(4) and squarefree_trial(6)
    assert rational_decimal('1.25')==QQ(5)/4
    assert contained(R(3),'3.000')
    assert not contained(R(QQ(3002)/1000),'3.000')
    assert R(1).agm(R(1)) == 1
    Khalf=R.pi()/(2*R(1).agm((R(1)/2).sqrt()))
    square=2*R(2).sqrt()*Khalf
    eta4,_=eta_period(-4,reduced_forms(-4))
    assert agrees(eta4,square,450)
    assert not agrees(eta4,square+R(10)**(-90),450)
    assert agrees(period(-4),square,400)
    b3=(R(QQ(3)/4)**(QQ(3)/4))*R(QQ(1)/3).gamma()**3/R.pi()
    assert agrees(period(-3),b3,400)
    b4=R(QQ(1)/4).gamma()**2/(2*R.pi()).sqrt()
    assert agrees(period(-4),b4,400)
    generator=ChowlaSelbergPeriods()
    params=list(generator.enumerate())
    Ds=[int(p['D']) for p in params]
    assert Ds==independent_discriminants(1000) and len(Ds)==305
    values={};records=[];etas={};forms_by_D={}; max_tail=R(0)
    stored=None
    if stored_path:
        stored=json.loads((HERE/stored_path).read_text())['Numbers']
        assert list(stored)==[str(D) for D in Ds]
    for D in Ds:
        forms=reduced_forms(D); forms_by_D[D]=forms
        chi,h,w=character_data(D)
        assert len(forms)==h==int(pari(D).qfbclassno())
        # Verify exact class-number relation and character parity over the full sum.
        assert sum(chi)==0
        assert all(chi[a-1]==-chi[-a] for a in range(1,-D))
        b=period(D);e,tail=eta_period(D,forms);etas[D]=e
        narrow(b,400);narrow(e,450)
        assert agrees(b,e,400),(D,b,e)
        assert not agrees(b,e+R(10)**(-90),400)
        text=to_text(b,100)
        assert contained(b,text) and contained(e,text),(D,text)
        if stored is not None:
            s=num(stored[str(D)])
            assert contained(b,s) and contained(e,s),(D,s)
        values[str(D)]=b
        max_tail=max(max_tail,tail)
        records.append({'D':D,'h':h,'w':w,'forms':forms,'number':text,
                        'gamma_bits':int(b.accuracy()),'eta_bits':int(e.accuracy()),
                        'working_bits':int(b.parent().precision()),
                        'gamma_enclosure':str(b),'eta_enclosure':str(e)})
    assert not exactness(values)
    assert not names_its_rings(str(HERE/'generate.py'))
    companion={}
    t230=optional('T230')
    if t230:
        negatives=[int(D) for D in t230['Numbers'] if int(D)<0]
        assert set(negatives)==set(Ds)
        for D in Ds:
            gammaK=written_ball(num(t230['Numbers'][str(D)]))
            expected=2*R.euler_constant()+(2*R.pi()**2).log()-2*etas[D].log()
            assert agrees(gammaK,expected,300),D
            if stored is not None:
                sb=written_ball(num(stored[str(D)]))
                assert agrees(gammaK,2*R.euler_constant()+(2*R.pi()**2).log()-2*sb.log(),300)
        companion['T230']=len(Ds)
    for tid,transform in [('T296',lambda x:2*R(2).sqrt()*x),
                          ('T506',lambda x:2*R.pi()*R(2).sqrt()*x**2)]:
        doc=optional(tid)
        if doc:
            result=transform(written_ball(num(doc['Numbers']['1'])))
            assert agrees(etas[-4],result,300)
            if stored is not None: assert agrees(written_ball(num(stored['-4'])),result,300)
            companion[tid]=1
    t177=optional('T177')
    if t177:
        beta=written_ball(num(t177['Numbers']['1/4']['1/2']))
        assert agrees(etas[-4],beta,300)
        if stored is not None: assert agrees(written_ball(num(stored['-4'])),beta,300)
        companion['T177']=1
    t129=optional('T129');t211=optional('T211')
    if t129 and t211:
        # Linear Hilbert polynomials are x-j, so their constant term gives j.
        from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
        P=PolynomialRing(QQ,'x'); j_to_D={}
        for D in Ds:
            row=t129['Numbers'].get(str(D))
            if row:
                poly=P(num(row))
                assert poly.degree()==len(forms_by_D[D])
                if poly.degree()==1: j_to_D[-poly[0]]=D
        found=[]
        for N,models in t211['Numbers'].items():
            for cc,quantities in models.items():
                if 'stable' not in quantities: continue
                c4,c6=map(ZZ,cc.split(','))
                j=QQ(1728)*c4**3/(c4**3-c6**2)
                if j not in j_to_D: continue
                D=j_to_D[j]
                height=R(-D).log()/4-etas[D].log()
                assert agrees(height,written_ball(num(quantities['stable'])),300),(N,cc,D)
                if stored is not None:
                    sb=written_ball(num(stored[str(D)]))
                    assert agrees(R(-D).log()/4-sb.log(),written_ball(num(quantities['stable'])),300)
                found.append({'entry':N+','+cc+',stable','D':D})
        assert found
        companion['T211']=found
    print(json.dumps({'table':'T669','count':len(Ds),'stored_checked':stored is not None,
      'skill_manifest':'agents/runs/20261102T045202Z-build-skills/manifest.json',
      'skill_sha256':'a2b28d9953264d7b0ca62663785c2952190c5be6d6d3597fd695d9a54d606278',
      'versions':{'python':platform.python_version(),'sage':sage.version.version,
                  'numberdb':__import__('numberdb').__version__,'pari':str(pari.version())},
      'gamma_min_bits':min(r['gamma_bits'] for r in records),
      'eta_min_bits':min(r['eta_bits'] for r in records),
      'max_log_F_tail':str(max_tail),'eta_terms':TERMS,'eta_working_bits':PRECISION,
      'working_bits':sorted({r['working_bits'] for r in records}),
      'measure_computed_balls':measure(values),'measure_written_values':measure({r['D']:r['number'] for r in records}),
      'class_number_one':[r['D'] for r in records if r['h']==1],
      'max_class_number':max(r['h'] for r in records),
      'odd_discriminants':sum(D%2!=0 for D in Ds),
      'even_discriminants':sum(D%2==0 for D in Ds),
      'known_controls_passed':True,'perturbation_rejections':len(Ds)+1,
      'eta_overlap_checks':len(Ds),'decimal_containment_checks':len(Ds),
      'companion_checks':companion,'records':records},indent=2))


if __name__=='__main__':
    main()
