"""Independent checks for T670; see generate.py for Sage invocation.

This check computes the eta class product without Gamma evaluations. Reduced
forms are enumerated directly, not obtained from the generator's class-number
formula. Each omitted log|eta| tail is explicitly bounded. It also checks
PARI class numbers, exact conductor corrections, saved/live companions, and
containment of entire computed balls in the written decimal intervals.

With no arguments, read companion T*.json exports beside this script when
present, otherwise fetch them from NumberDB. --stored additionally fetches
T670 or reads stored-T670.json. NUMBERDB_STORED=1 is the campaign equivalent.
The campaign archived skill manifest is recorded in skill-evidence.json.
"""
import os
import sys
import re
import json
import math
from pathlib import Path
from decimal import Decimal
from fractions import Fraction
from functools import lru_cache
import numberdb.sage as numberdb
from numberdb._write import to_text
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
from sage.rings.real_arb import RealBallField
from sage.libs.pari import pari
from sage.env import SAGE_VERSION
import generate
from check import exactness, measure, names_its_rings

HERE = Path(__file__).resolve().parent
R = RealBallField(512)
M = 80


def finite_narrow(x, threshold=115):
    assert x.is_finite(), 'nonfinite ball'
    assert R(x.rad()) < R(10)**(-threshold), 'ball too wide'
    return x


def agreement(x, y, label):
    finite_narrow(x, 95)
    finite_narrow(y, 95)
    assert x.overlaps(y), 'Disagreement: '+label


def rational_decimal(s):
    f = Fraction(s)
    return QQ(f.numerator)/f.denominator


def written_parts(s):
    s = str(s)
    center = rational_decimal(s)
    ulp = QQ(10)**Decimal(s).as_tuple().exponent
    return center, ulp


def written_ball(s):
    mid, ulp = written_parts(s)
    return R(mid).add_error(R(ulp))


def enclosed_by_written(x, s):
    finite_narrow(x)
    mid, ulp = written_parts(s)
    assert mid-ulp <= QQ(x.lower()) and QQ(x.upper()) <= mid+ulp, 'Written interval does not contain full ball'


@lru_cache(maxsize=None)
def reduced_forms(delta):
    forms=[]
    for a in range(1, math.isqrt((-delta)//3)+1):
        for b in range(-a,a+1):
            if (b*b-delta) % (4*a):
                continue
            c=(b*b-delta)//(4*a)
            if c<a or math.gcd(math.gcd(a,b),c)!=1:
                continue
            if (abs(b)==a or a==c) and b<0:
                continue
            forms.append((a,b,c))
    assert forms
    return tuple(forms)


@lru_cache(maxsize=None)
def eta_log_F(delta,a,b):
    y=R(-delta).sqrt()/(2*a)
    x=R(QQ(-b)/(2*a))
    r=(-2*R.pi()*y).exp()
    answer=y.log()/2 - R.pi()*y/6
    rn=R(1)
    for n in range(1,M+1):
        rn *= r
        answer += (1-2*rn*(2*R.pi()*n*x).cos()+rn*rn).log()
    tail=2*r**(M+1)/((1-r)*(1-r**(M+1)))
    return finite_narrow(answer.add_error(tail),140), tail


@lru_cache(maxsize=None)
def eta_height(delta):
    forms=reduced_forms(delta)
    ans=-(2*R.pi()).log()-sum((eta_log_F(delta,a,b)[0] for a,b,c in forms),R(0))/len(forms)
    return finite_narrow(ans,140)


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


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


def independent_decomposition(delta):
    candidates=[]
    for f in range(1,math.isqrt((-delta)//3)+1):
        if delta % (f*f):
            continue
        D=delta//(f*f)
        if D%4==1:
            k=D
        elif D%4==0 and (D//4)%4 in (2,3):
            k=D//4
        else:
            continue
        if all((-k)%(p*p) for p in range(2,math.isqrt(-k)+1)):
            candidates.append((D,f))
    assert len(candidates)==1
    return candidates[0]


def main():
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        os.environ['NUMBERDB_API_KEY']=sys.stdin.read().strip()
    generator=generate.StableCMHeights()
    # Controls precede measurements; the AGM identity fixes the metric scale.
    assert R(1).agm(R(1))==1
    assert (R.pi()/(2*R(1).agm(R(1)))).overlaps(R.pi()/2)
    K=R.pi()/(2*R(1).agm((R(1)/2).sqrt()))
    agreement(eta_log_F(-4,1,0)[0],(K/R.pi()).log(),'square-lattice eta/AGM')
    agreement(eta_height(-4),-(2*K).log(),'square-lattice height/AGM')
    assert reduced_forms(-3)==((1,1,1),)
    assert reduced_forms(-4)==((1,0,1),)
    assert len(reduced_forms(-23))==3
    assert len(reduced_forms(-12))==1
    assert not eta_height(-4).overlaps(eta_height(-4)+R(10)**-90)
    print('Controls passed: known AGM, square-lattice metric, primitive forms, rejected perturbation',flush=True)
    assert not names_its_rings(str(HERE/'generate.py'))
    enumeration=list(generator.enumerate())
    deltas=[int(p['Delta']) for p in enumeration]
    expected=[-d for d in range(1,1001) if (-d)%4 in (0,1)]
    assert deltas==expected and len(set(deltas))==500
    values={}; text_values={}; details=[]; roles={-1:0,0:0,1:0}
    min_acc=10000; min_eta=10000; max_tail=R(0); max_radius=R(0)
    nonmax=0; max_f=0; prime_powers=0; composite_support=0
    for i,params in enumerate(enumeration):
        delta=int(params['Delta']); raw=generator.value(params,100)
        v=raw['number'] if isinstance(raw,dict) else raw
        finite_narrow(v)
        D,f=independent_decomposition(delta)
        assert (D,f)==tuple(map(int,generate.decompose(delta)))
        forms=reduced_forms(delta)
        assert len(forms)==int(pari(delta).qfbclassno())
        hD=len(reduced_forms(D)); w=6 if D==-3 else 4 if D==-4 else 2
        assert hD==int(generate.field_data(D)[2])
        # Ring class number and conductor correction, using the paper's e(p).
        ring_class=QQ(hD*f)
        e_sum=R(0)
        factors=list(ZZ(f).factor())
        if f>1:
            nonmax+=1
            ring_class /= QQ(w)/2
        if any(vp>1 for p,vp in factors): prime_powers+=1
        if len(factors)>1: composite_support+=1
        max_f=max(max_f,f)
        for p,vp in factors:
            chi=int(pari(D).kronecker(p))
            roles[chi]+=1
            ring_class *= 1-QQ(chi)/p
            e=QQ((1-p**vp)*(1-chi))/(p**(vp-1)*(1-p)*(chi-p))
            c=QQ(1-chi)/(p-chi)*(1-QQ(1)/p**vp)/(1-QQ(1)/p)
            assert e==-c
            e_sum+=R(e)*R(p).log()/2
        assert ring_class==len(forms)
        corr=R(f).log()/2+e_sum
        agreement(v,eta_height(D)+corr,'conductor relation '+str(delta))
        other=eta_height(delta)
        agreement(v,other,'Gamma vs eta '+str(delta))
        assert not (v+R(10)**-90).overlaps(other), 'perturbation should fail'
        written=to_text(v,digits=100)
        enclosed_by_written(v,written); enclosed_by_written(other,written)
        values[str(delta)]=raw; text_values[str(delta)]={'number':written}
        if isinstance(raw,dict): text_values[str(delta)]['comment']=raw['comment']
        min_acc=min(min_acc,int(v.accuracy())); min_eta=min(min_eta,int(other.accuracy()))
        max_radius=max(max_radius,R(v.rad()))
        for a,b,c in forms: max_tail=max(max_tail,eta_log_F(delta,a,b)[1])
        details.append({'Delta':delta,'D':D,'f':f,'class_number':len(forms),'generator_bits':int(v.accuracy()),'eta_bits':int(other.accuracy()),'number':written,'eta_enclosure':str(other)})
        if (i+1)%100==0: print('Checked',i+1,'orders',flush=True)
    assert nonmax==195 and max_f==18
    assert not exactness(values)
    measurements=measure(values)
    # Published companions and sibling, read afresh or from named API exports.
    cm_periods=document('T669')['Numbers']; ek=document('T230')['Numbers']
    fields=[d for d in deltas if independent_decomposition(d)[1]==1]
    assert set(map(str,fields))==set(cm_periods)
    assert set(fields)=={int(k) for k in ek if int(k)<0}
    for D in fields:
        agreement(written_ball(number(cm_periods[str(D)])).log(),R(-D).log()/4-eta_height(D),'T669 period '+str(D))
        ek_height=(written_ball(number(ek[str(D)]))-2*R.euler_constant()-(2*R.pi()**2).log())/2 + R(-D).log()/4
        agreement(eta_height(D),ek_height,'T230 '+str(D))
    hp=document('T129')['Numbers']; j_to_delta={}
    for key,entry in hp.items():
        polynomial=number(entry)
        degree=max([int(n) for n in re.findall(r'x\^(\d+)',polynomial)]+([1] if 'x' in polynomial else [0]))
        assert degree==len(reduced_forms(int(key)))
        if degree==1:
            compact=polynomial.replace(' ','')
            assert compact=='x' or re.fullmatch(r'x[+-]\d+',compact)
            j_to_delta[-int(compact[1:] or '0')]=int(key)
    t211=document('T211')['Numbers']; curve_checks=[]
    for N,curves in t211.items():
        for invariants,entries in curves.items():
            c4,c6=map(int,invariants.split(','))
            j=QQ(1728*c4**3)/(c4**3-c6**2)
            if j in j_to_delta and 'stable' in entries:
                delta=j_to_delta[j]
                agreement(eta_height(delta),written_ball(number(entries['stable'])),'T211 '+N+','+invariants)
                curve_checks.append({'entry':N+','+invariants+',stable','Delta':delta})
    assert len(curve_checks)>=10
    for r in [1,4]:
        delta=-4*r
        assert len(reduced_forms(delta))==1
        stored_eta=written_ball(number(document('T506')['Numbers'][str(r)]))
        expected_eta=-(2*R.pi()).log()-R(r).log()/4-2*stored_eta.log()
        agreement(eta_height(delta),expected_eta,'T506 '+str(r))
    agreement(eta_height(-4),-(2*written_ball(number(document('T296')['Numbers']['1']))).log(),'T296 AGM value')
    # The paper's rounded example is a low-precision normalization control.
    assert written_ball('-0.939042336039478').overlaps(eta_height(-28))
    stored_count=0
    if '--stored' in sys.argv or os.environ.get('NUMBERDB_STORED')=='1':
        stored=document('T670',stored=True)
        block=stored['Numbers']
        assert list(block)==list(text_values)
        assert len(block)==500
        for key,entry in block.items():
            v=number(entry)
            enclosed_by_written(eta_height(int(key)),v)
            enclosed_by_written(generate.height(int(key),429),v)
            # The independent conductor/period identity on read-back intervals.
            D,f=independent_decomposition(int(key))
            corr=generate.correction(D,f,R)
            agreement(written_ball(v),R(-D).log()/4-written_ball(number(cm_periods[str(D)])).log()+corr,'stored period identity '+key)
            assert not (written_ball(v)+R(10)**-90).overlaps(eta_height(int(key)))
            stored_count+=1
    evidence={'table':'T670','stage':'build','skill_manifest':'agents/runs/20261102T053137Z-build-skills/manifest.json','skills':json.loads((HERE/'skill-evidence.json').read_text()),'sage_version':SAGE_VERSION,'numberdb_version':numberdb.__version__,'working_bits':429,'eta_bits':512,'eta_terms':M,'entries':500,'maximal_orders':305,'nonmaximal_orders':nonmax,'max_conductor':max_f,'conductor_prime_occurrences_by_character':roles,'orders_with_prime_power_conductor_factor':prime_powers,'orders_with_multiple_conductor_primes':composite_support,'minimum_generator_accuracy_bits':min_acc,'minimum_eta_accuracy_bits':min_eta,'max_generator_radius':str(max_radius),'max_log_F_tail_bound':str(max_tail),'measure':measurements,'written_longest':max(len(v['number']) for v in text_values.values()),'written_json_bytes':len(json.dumps(text_values).encode()),'eta_agreements':500,'exact_PARi_class_numbers':500,'exact_ring_class_numbers':500,'generator_and_eta_containment':500,'negative_controls_rejected':500,'T669_period_links_checked':305,'T230_height_links_checked':305,'T129_form_counts_checked':len(hp),'T211_CM_controls':curve_checks,'T506_rectangular_controls':[1,4],'stored_entries_checked':stored_count,'per_entry':details}
    print('EVIDENCE_JSON '+json.dumps(evidence,sort_keys=True),flush=True)


if __name__=='__main__':
    main()
