"""Independent numerical and selection checks for numberdb.org/T677.

Run: sage -python checks.py > numerical-evidence.json
Set NUMBERDB_CHECK_STORED=1 and NUMBERDB_API_KEY to check the private draft too.
Uses generate.py and the campaign's check.py, both attached to the table.

The independent sum uses exact integer convergents, an independent complete-
quotient recurrence, and 4096-bit Arb arithmetic. After 800 terms its absolute
remainder is bounded by 4*log(M+1)*2^-400; all rounding error is also enclosed.
"""
import numberdb.sage as numberdb
from sage.rings.real_arb import RealBallField
from sage.rings.real_mpfi import RealIntervalField
from sage.rings.rational_field import QQ
from math import gcd, isqrt
from fractions import Fraction
from decimal import Decimal
from collections import Counter
from pathlib import Path
import json, os, hashlib
import generate
from check import exactness, measure, names_its_rings
from numberdb._write import to_text

TERMS = 800
CHECK_BITS = 4096
SKILL_SHA = 'a2b28d9953264d7b0ca62663785c2952190c5be6d6d3597fd695d9a54d606278'
SKILL_MANIFEST = 'agents/runs/20261102T115856Z-build-skills/manifest.json'


def independent_selection(bound):
    out = set()
    for D in range(5, bound+1):
        if D % 4 not in (0, 1) or isqrt(D)**2 == D:
            continue
        for B in range(1, isqrt(D)+1):
            if (D-B*B) % 4:
                continue
            product = (D-B*B)//4
            for A in range(1, product+1):
                if product % A:
                    continue
                K = product//A
                if gcd(gcd(A,B),K) == 1 and abs(A-K) < B:
                    out.add((A,B,K))
    return out


def independent_period(t):
    A,B,K = t
    D = B*B+4*A*K
    h,k = B,2*K
    start=(h,k); seen=set(); states=[]; digits=[]
    while (h,k) not in seen:
        seen.add((h,k));states.append((h,k))
        a=(isqrt(D)+h)//k
        digits.append(a)
        h=a*k-h
        assert (D-h*h)%k == 0
        k=(D-h*h)//k
        assert k>0
    assert (h,k)==start
    return D,states,digits


def series(t, unit_weight=False):
    F=RealBallField(CHECK_BITS)
    D,states,digits=independent_period(t)
    A,B,K=t
    x=(F(D).sqrt()-B)/(2*A)
    logs=[F(1) if unit_weight else ((F(D).sqrt()+h)/k).log()
          for h,k in states]
    pm2,pm1,qm2,qm1=1,0,0,1
    total=F(0)
    for j in range(TERMS):
        beta=F(1) if j==0 else abs(qm2*x-pm2)
        total += beta*logs[j%len(states)]
        a=digits[j%len(digits)]
        pm2,pm1=pm1,a*pm1+pm2
        qm2,qm1=qm1,a*qm1+qm2
    tail=4*(F(1) if unit_weight else F(max(digits)+1).log())*F(2)**(-TERMS//2)
    I=RealIntervalField(CHECK_BITS)
    total_interval=I(total)
    # Brjuno summands are positive, so the one-sided remainder lies in [0,tail].
    enclosed=total_interval + I(0,I(tail).upper())
    assert total.is_finite() and tail.is_finite()
    assert I(enclosed.absolute_diameter()) < I(10)**(-118)
    return enclosed, tail, digits


def written_interval(text):
    d=Decimal(text);v=Fraction(d);ulp=Fraction(10)**d.as_tuple().exponent
    q=lambda f: QQ(f.numerator)/QQ(f.denominator)
    return RealIntervalField(CHECK_BITS)(q(v-ulp),q(v+ulp)),q(ulp)


def inside(inner, outer):
    return outer.lower() <= inner.lower() and inner.upper() <= outer.upper()


def main():
    F=RealBallField(CHECK_BITS); I=RealIntervalField(CHECK_BITS)
    controls=[]
    for a in (1,2):
        x=(F(a*a+4).sqrt()-a)/2
        unity,_,_=series((1,a,1),unit_weight=True)
        known=I(1/(1-x))
        assert unity.overlaps(known)
        assert unity.lower()>1
        analytic=I((-x.log())/(1-x))
        actual,_,_=series((1,a,1))
        assert actual.overlaps(analytic)
        controls.append({'partial_quotient':a,'unit_weight_control':str(unity),'logarithmic_control':str(actual)})
    ts=generate.triples()
    assert len(ts)==len(set(ts))==141
    assert set(ts)==independent_selection(100)
    cycles=set();lengths=Counter();discriminants=set();data={};certificates=[]
    minimum_accuracy=10000
    for t in ts:
        D,states,ds=independent_period(t)
        orbit,gs=generate.cycle(t)
        assert ds==gs and set(orbit)<=set(ts)
        assert len(set(orbit))==len(orbit)
        for cur in orbit:
            A,B,K=cur
            assert B*B+4*A*K==D and gcd(gcd(A,B),K)==1
            # Exact root condition plus stable primitive minimal polynomial.
            assert K>0 and A+B-K>0 and A-B-K<0
        cycles.add(min(orbit)); lengths[len(ds)]+=1;discriminants.add(D)
        key=generate.identity(t)
        val=generate.BrjunoValues().value({'x':key},100)
        data[key]=val
        b=val['number'];assert b.is_finite()
        minimum_accuracy=min(minimum_accuracy,int(b.accuracy()))
        text=to_text(b,100);outer,ulp=written_interval(text)
        assert inside(I(b),outer)
        alt,tail,_=series(t)
        assert inside(alt,outer) and alt.overlaps(I(b))
        nxt,_=generate.step(t)
        x=generate.root(t,b.parent())
        residual=b+x.log()-x*generate.brjuno(nxt)
        assert residual.is_finite() and residual.contains_zero()
        assert I(abs(residual).upper())<I(10)**(-120)
        certificates.append({'x':key,'polynomial':[t[0],t[1],-t[2]],'D':D,
                             'period':ds,'orbit':[generate.identity(c) for c in orbit],
                             'number':text,'accuracy_bits':int(b.accuracy()),
                             'series_interval':str(alt),'tail_bound':str(tail),
                             'series_width_in_ulp':str(alt.absolute_diameter()/ulp)})
    assert dict(lengths)=={1:9,2:28,3:21,4:24,5:10,6:24,7:7,9:18}
    assert len(cycles)==45 and len(discriminants)==40
    assert not exactness(data)
    assert not names_its_rings(str(Path(__file__).with_name('generate.py')))
    stored_checks=None
    if os.environ.get('NUMBERDB_CHECK_STORED')=='1':
        doc=numberdb.table('T677')
        block=doc['Numbers']
        assert set(block)==set(data)
        stored_values={}
        for t in ts:
            key=generate.identity(t);row=block[key]
            text=row['number'] if isinstance(row,dict) else row
            outer,ulp=written_interval(text)
            alt,_,_=series(t)
            assert inside(alt,outer)
            assert inside(I(data[key]['number']),outer)
            assert row['param-latex']==data[key]['param-latex']
            stored_values[key]=outer
        for t in ts:
            key=generate.identity(t);nxt,_=generate.step(t);x=I(generate.root(t,F))
            residual=stored_values[key]+x.log()-x*stored_values[generate.identity(nxt)]
            assert residual.contains_zero()
            assert I(residual.absolute_diameter())<I(10)**(-97)
        # Published regulator's original written interval supplies an external control.
        regulator=numberdb.table('T131')['Numbers']['5']['number']
        R,_=written_interval(regulator)
        x=I((F(5).sqrt()-1)/2)
        assert stored_values['quad_1_1_m1'].overlaps(R/(1-x))
        stored_checks={'entries':141,'series_containment':141,'functional_equations':141,
                       'golden_regulator_T131_5':True}
    result={'table':'T677','skill_manifest':SKILL_MANIFEST,'skill_sha256_local':SKILL_SHA,
            'skill_sha256_live':SKILL_SHA,'environment':generate.BrjunoValues().environment(),
            'entries':len(ts),'cycles':len(cycles),'discriminants':len(discriminants),
            'period_length_counts':dict(lengths),'maximum_partial_quotient':max(max(c['period']) for c in certificates),
            'minimum_generator_accuracy_bits':minimum_accuracy,
            'generator_working_bits':numberdb.bits(100,losing=generate.WORKING_GUARD),
            'independent_terms':TERMS,'independent_bits':CHECK_BITS,'controls':controls,
            'measurement':measure(data),'written_longest':max(len(c['number']) for c in certificates),
            'functional_equations':141,'independent_series_containment':141,'stored_checks':stored_checks,
            'generator_sha256':hashlib.sha256(Path(__file__).with_name('generate.py').read_bytes()).hexdigest(),
            'certificates':certificates}
    print(json.dumps(result,indent=2))

if __name__=='__main__':
    main()
