"""Independent checks for Bettin–Conrey cotangent sums, numberdb.org/T639.

    sage -python checks.py
    sage -python checks.py --stored       # NUMBERDB_API_KEY required for draft

The exact check continues the Estermann series through Hurwitz zeta values
zeta(0,a/k)=1/2-a/k. No cotangent occurs in that computation. A second
numerical check uses Arb's digamma implementation and the reflection formula.
Controls are tested before the full range. Every compared enclosure must be
finite and narrow. Stored decimal intervals are checked for containment of
higher precision independent balls, not just agreement of their midpoints.
"""
import json
import sys
from functools import lru_cache
from math import gcd, lcm
import numberdb.sage as numberdb
from numberdb._write import to_text
from numberdb._generate import _stored_entries
from sage.rings.number_field.number_field import CyclotomicField
from sage.rings.rational_field import QQ
from sage.rings.complex_arb import ComplexBallField
from sage.rings.real_arb import RealBallField
from generate import BettinConrey, algebraic_sum, cotangent_ball

PRECISION = 550
C = ComplexBallField(PRECISION)
B = RealBallField(PRECISION)

def plain(value):
    return value['number'] if isinstance(value, dict) else value

def narrow(value, relative=False):
    assert value.is_finite(), 'Nonfinite comparison'
    if relative and not value.contains_zero():
        assert value.rad() < abs(value).lower() * (QQ(1) / 10**120)
    else:
        assert value.rad() < QQ(1) / 10**120
    return value

@lru_cache(maxsize=None)
def estermann_exact(k, h):
    n = lcm(k, 4)
    field = CyclotomicField(n)
    z = field.gen()
    i = z**(n//4)
    root = z**(n//k)
    coefficients = [0] * k
    for a in range(1, k+1):
        for b in range(1, k+1):
            coefficients[(h*a*b) % k] += (k-2*a) * (k-2*b)
    D = sum(QQ(c) * root**j for j,c in enumerate(coefficients)) / (4*k*k)
    # The conjugate here is the exact cyclotomic automorphism z -> z^-1.
    conjugate = field.hom([1/z])(D)
    assert D + conjugate == QQ(1)/2
    return (D - conjugate)/i, D

def exact_ball(value):
    embedded = C(value)
    narrow(embedded.real())
    narrow(embedded.imag())
    assert embedded.imag().contains_zero()
    return embedded.real()

@lru_cache(maxsize=None)
def digammas(k):
    return [None]+[C(QQ(r)/k).psi().real() for r in range(1,k)]

def digamma_value(k,h):
    psi=digammas(k)
    value=sum(B(QQ(m)/k)*(psi[(m*h)%k]-psi[k-(m*h)%k])
              for m in range(1,k))/B.pi()
    return narrow(value)

def vasyunin(k,h):
    value=B(0)
    for m in range(1,k):
        if 2*m==k:
            continue
        value += B(QQ((m*h)%k)/k) * (B.pi()*B(QQ(m)/k)).cot()
    return narrow(value)

def decimal_rational(text):
    from decimal import Decimal
    tup=Decimal(text).as_tuple()
    integer=int(''.join(map(str,tup.digits))) * (-1 if tup.sign else 1)
    return QQ(integer)*QQ(10)**int(tup.exponent)

def stored_interval(text):
    text=str(text).strip()
    if '.' not in text and 'e' not in text.lower():
        return QQ(text),QQ(text)
    from decimal import Decimal
    centre=decimal_rational(text)
    unit=QQ(10)**int(Decimal(text).as_tuple().exponent)
    return centre-unit,centre+unit

def main(stored_document=None):
    # Nonzero and zero controls, evaluated before any comparison is accepted.
    psi_control=C(QQ(1)/2).psi().real()
    expected=-B.euler_constant()-2*B(2).log()
    narrow(psi_control);narrow(expected)
    assert psi_control.overlaps(expected) and psi_control < -1
    e2,D2=estermann_exact(2,1)
    e4,D4=estermann_exact(4,1)
    assert e2==0 and D2==QQ(1)/4
    assert e4==QQ(1)/2 and D4==QQ(1)/4+D4.parent().gen()*QQ(1)/4
    assert (QQ(1)/2) in digamma_value(4,1)
    assert (-QQ(1)/2) in vasyunin(4,1)
    control3=exact_ball(estermann_exact(3,1)[0])
    assert control3.overlaps(1/(3*B(3).sqrt()))
    assert not control3.overlaps(-control3)
    assert not control3.overlaps(control3+B(QQ(1)/10**100))
    assert exact_ball(estermann_exact(6,1)[0]).overlaps(7/(3*B(3).sqrt()))
    generator=BettinConrey()
    selected=list(generator.enumerate())
    numbers={};exact_rationals={};max_radius=B(0);min_accuracy=10000
    for params in selected:
        k,h=int(params['k']),int(params['h'])
        value=plain(generator.value(params,100))
        exact,D=estermann_exact(k,h)
        assert exact == algebraic_sum(k,h), (k,h,'exact Estermann disagreement')
        embed=exact_ball(exact)
        digamma=digamma_value(k,h)
        assert embed.overlaps(digamma),(k,h,'digamma disagreement')
        if exact in QQ:
            assert value==QQ(exact)
            exact_rationals[f'{k},{h}']=str(QQ(exact))
        else:
            narrow(value,relative=True)
            assert value.overlaps(embed)
            max_radius=max(max_radius,B(value.rad()))
            min_accuracy=min(min_accuracy,int(value.accuracy()))
        assert narrow(cotangent_ball(k,h+k,120)).overlaps(embed)
        numbers[k,h]=B(value)
    for k,h in numbers:
        assert (numbers[k,h]+numbers[k,k-h]).contains_zero()
        assert (numbers[k,pow(h,-1,k)]+vasyunin(k,h)).contains_zero()
    records=[{'params':p,'number':to_text(plain(generator.value(p,100)),digits=100)} for p in selected]
    stored_count=0
    if stored_document is not None:
        assert tuple(stored_document['Parameters']) == ('k','h')
        live=[{'params':dict(zip(('k','h'),identity.split(','))), 'number':value}
              for identity,value in _stored_entries(stored_document).items()]
        assert [tuple(int(r['params'][p]) for p in ('k','h')) for r in live] == list(numbers)
        intervals={}
        for record in live:
            k,h=(int(record['params'][p]) for p in ('k','h'))
            lo,hi=stored_interval(record['number'])
            independent=exact_ball(estermann_exact(k,h)[0])
            if lo==hi:
                assert estermann_exact(k,h)[0]==lo
            else:
                assert lo<=independent.lower() and independent.upper()<=hi,(k,h,'not contained')
            intervals[k,h]=(lo,hi)
            stored_count+=1
        for (k,h),(lo,hi) in intervals.items():
            rlo,rhi=intervals[k,k-h]
            assert lo+rlo<=0<=hi+rhi
            ilo,ihi=intervals[k,pow(h,-1,k)]
            V=vasyunin(k,h)
            assert ilo<=-V.upper() and -V.lower()<=ihi if ilo!=ihi else (-ilo) in V
    from sage.version import version as sage_version
    import platform
    result={'versions':{'sage':sage_version,'numberdb':numberdb.__version__,'python':platform.python_version()},'table':'T639','entries':len(selected),'exact_rationals':exact_rationals,
            'exact_estermann_checks':len(selected),'digamma_checks':len(selected),
            'periodicity_checks':len(selected),'reflection_checks':len(numbers),
            'vasyunin_checks':len(numbers),'controls':'known zero, nonzero, sign-flip and 1e-100 perturbation controls passed before range checks',
            'minimum_accuracy_bits':min_accuracy,'maximum_absolute_ball_radius':str(max_radius),
            'maximum_stored_characters':max(len(r['number']) for r in records),
            'stored_independent_containment_checks':stored_count,
            'precision_bits_independent':PRECISION,
            'skill_manifest':'agents/runs/20261031T200151Z-build-skills/manifest.json',
            'skill_local_sha256':'a2b28d9953264d7b0ca62663785c2952190c5be6d6d3597fd695d9a54d606278',
            'skill_live_sha256':'a2b28d9953264d7b0ca62663785c2952190c5be6d6d3597fd695d9a54d606278'}
    print('EVIDENCE_JSON '+json.dumps(result,sort_keys=True))
    print('NUMBERS_JSON '+json.dumps(records))
    return result

if __name__=='__main__':
    document=numberdb.table('T639') if '--stored' in sys.argv else None
    main(document)
