"""Independent numerical checks for numberdb.org/T586; never writes the table.

Run `sage -python checks.py` beside the generator and its dependencies.
The default reads every stored entry and runs independent log-gamma/zeta
product checks, mpmath checks at 125 and 155 digits, and mutation controls.
A draft needs NUMBERDB_API_KEY, or a key piped to stdin with
NUMBERDB_KEY_FROM_STDIN=1. The build calls run_checks(rows) before filling.
"""
import os
import sys
import json
import time
from decimal import Decimal
from importlib.metadata import version
from fractions import Fraction
from pathlib import Path
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from sage.rings.real_arb import RealBallField
from sage.rings.complex_arb import ComplexBallField
from sage.env import SAGE_VERSION
import mpmath as mp
from generate import HardyZValues, argument, value_ball, bridge
from stationary import isolate
from check import exactness, measure, names_its_rings

R=RealBallField(620)
C=ComplexBallField(620)

def rat(text):
    q=Fraction(Decimal(str(text)))
    return QQ(q.numerator)/QQ(q.denominator)

def decimal_interval(text):
    d=Decimal(text)
    unit=QQ(10)**d.as_tuple().exponent
    midpoint=rat(text)
    return midpoint-unit,midpoint+unit,unit

def contained(v,a,b):
    return v.is_finite() and v > R(a) and v < R(b)

def narrow(v,bits=350):
    assert v.is_finite() and v.accuracy()>=bits
    return v

def product(t, derivative=False):
    t=R(t)
    theta=C(QQ(1)/4,t/2).log_gamma().imag()-t*R.pi().log()/2
    e=C(0,theta).exp()
    s=C(QQ(1)/2,t)
    if derivative:
        theta1=(C(QQ(1)/4,t/2).psi().real()-R.pi().log())/2
        z=C(0,1)*e*(theta1*s.zeta()+s.zetaderiv(1))
    else:
        z=e*s.zeta()
    assert z.real().is_finite() and z.imag().is_finite() and z.imag().contains_zero()
    return z.real()

def mpq(q):
    return mp.mpf(str(q.numerator()))/mp.mpf(str(q.denominator()))

def run_checks(rows):
    started=time.time()
    generator=HardyZValues()
    expected=[p['t'] for p in generator.enumerate()]
    assert [r['t'] for r in rows]==expected
    assert len(set(expected))==303
    assert not names_its_rings(str(Path(__file__).with_name('generate.py')))
    # Known-answer and deliberate corruption controls precede all comparisons.
    origin=product(R(0))
    assert contained(origin,QQ(-1461)/1000,QQ(-1460)/1000)
    assert (origin-C(QQ(1)/2).zeta().real()).contains_zero()
    assert product(R(0),True).contains_zero()
    assert not contained(origin,QQ(1460)/1000,QQ(1461)/1000)
    intervals={r['t']:decimal_interval(r['number']) for r in rows}
    min_accuracy=10000
    bad_gram=[]
    curvature=[]
    for k,t in enumerate(expected):
        a,b,unit=intervals[t]
        x=argument(t,120)
        v=value_ball(t,120)
        w=narrow(product(x))
        assert contained(v,a,b) and contained(w,a,b),(t,'decimal containment')
        assert v.overlaps(w)
        min_accuracy=min(min_accuracy,int(w.accuracy()))
        assert not contained(w,a+10*unit,b+10*unit)
        assert not contained(w,a-10*unit,b-10*unit)
        negative=narrow(product(-x))
        assert negative.overlaps(w),(t,'evenness')
        if t.startswith('gram:'):
            n=int(t.split(':')[1])
            z=C(QQ(1)/2,R(x)).zeta()
            other=(-1 if n%2 else 1)*z.real()
            assert z.imag().contains_zero() and other.is_finite()
            assert contained(other,a,b) and w.overlaps(other)
            if (-1 if n%2 else 1)*w<0:bad_gram.append(n)
        elif t.startswith('stationary:'):
            lo=QQ(x.lower());hi=QQ(x.upper())
            left=product(R(lo),True);right=product(R(hi),True)
            assert (left<0<right) or (right<0<left),(t,'independent root certificate')
            assert product(x,True).contains_zero()
            z2=2*bridge().jet(x,3,620)[2].real()
            assert z2.is_finite() and not z2.contains_zero()
            curvature.append(1 if z2>0 else -1)
        if (k+1)%50==0:print('Product and identity checks:',k+1,flush=True)
    assert bad_gram==[126,134,195]
    assert curvature[:2]==[-1,1]
    # Hall's coarse descriptions are external checks on the two low extrema.
    assert contained(product(argument('stationary:1',120)),QQ(-53)/100,QQ(-52)/100)
    assert contained(product(argument('stationary:2',120)),QQ(-156)/100,QQ(-155)/100)
    # Different library, with independently solved roots at each precision.
    # Coarse certified brackets only select the intended stationary root.
    for dps in (125,155):
        mp.mp.dps=dps
        assert abs(mp.siegelz(0)-mp.zeta(mp.mpf(1)/2)) < mp.mpf(10)**(-dps+5)
        assert mp.mpf('-1.461')<mp.siegelz(0)<mp.mpf('-1.460')
        for k,t in enumerate(expected):
            if t=='0':x=mp.mpf(0)
            else:
                family,idx=t.split(':');idx=int(idx)
                if family=='gram':x=mp.grampoint(idx)
                else:
                    lo,hi=isolate()[0][idx-1]
                    x=mp.findroot(lambda y:mp.siegelz(y,derivative=1),
                                  (mpq(lo),mpq(hi)),solver='secant')
                    assert mpq(lo)<x<mpq(hi)
                    assert abs(mp.siegelz(x,derivative=1))<mp.mpf(10)**(-dps+10)
            y=mp.siegelz(x)
            a,b,unit=intervals[t]
            assert mpq(a)<y<mpq(b),(t,dps,'mpmath value')
            arg=argument(t,120)
            if t!='0':
                # 125-digit computations need not lie in a 140-digit ball;
                # compare at 110 decimal digits, still beyond stored precision.
                assert abs(x-mpq(QQ(arg.mid()))) < mp.mpf(10)**(-110)
            if (k+1)%100==0:print('mpmath',dps,'digits:',k+1,flush=True)
    assert not exactness({t:value_ball(t,120) for t in expected})
    values={r['t']:r['number'] for r in rows}
    return {'table':'T586','entries':len(rows),'product_checks':303,
            'parity_checks':303,'gram_identity_checks':202,
            'gram_sign_failures':bad_gram,'stationary_endpoint_certificates':100,
            'mpmath_checks':{'125_digits':303,'155_digits':303},
            'shifted_intervals_rejected':606,'controls':'passed',
            'min_independent_product_accuracy_bits':min_accuracy,
            'measure':measure(values),'cover':isolate()[1],
            'versions':{'sage':SAGE_VERSION,'numberdb':version('numberdb'),
                        'mpmath':mp.__version__,'flint':bridge().flint_version()},
            'seconds':time.time()-started,
            'policy_manifest':'agents/runs/20261028T165752Z-build-skills/manifest.json',
            'skill_sha256':'e37efb7e2ededf6c1d7134c673f152f648f26e0267c556aebdf3ec65765d8b4c'}

if __name__=='__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        key=sys.stdin.read().strip()
        if '=' in key:key=key.split('=',1)[1].strip().strip('\"\'')
        os.environ['NUMBERDB_API_KEY']=key
    document=numberdb.table('T586')
    # This table uses flat records. Read the actual stored decimal strings.
    rows=[{'t':r['params']['t'],'number':r['number']} for r in document['Numbers']]
    evidence=run_checks(rows)
    print('CHECKS_JSON '+json.dumps(evidence))
