"""Independent checks for T622, run with sage -python checks.py.

Default: compute the generator locally, then check it by a cotangent sum in
512-bit Arb, Dedekind reciprocity, and weighted lattice-point enumeration.
--stored: read the table through the API and run every check on stored values.
NUMBERDB_KEY_FROM_STDIN=1 selects --stored in the campaign wrapper.

This file and the unchanged campaign check.py are attached to the draft.
The output following EVIDENCE_JSON is a complete reproducible evidence object.
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from sage.rings.real_arb import RealBallField
from sage.env import SAGE_VERSION
from fractions import Fraction
from functools import lru_cache
from math import gcd
import hashlib
import json
import os
from pathlib import Path
import sys
import generate
from check import exactness, measure, agrees_with, names_its_rings

BITS = 512
R = RealBallField(BITS)


def cotangent_eta(p, q, u):
    """Independent trigonometric representation, without sawtooth arithmetic."""
    total = R(0)
    for j in range(1, p):
        a = R.pi()*R(j)/R(p)
        b = R.pi()*R(q*j)/R(p)
        c = R.pi()*R(2*u*j)/R(p)
        total += a.cos()/a.sin() * b.cos()/b.sin() * c.cos()
    return -total/R(p)


@lru_cache(None)
def dedekind_reciprocity(a, b):
    """Classical Dedekind sum from reciprocity, using only Python Fractions."""
    if b == 1:
        return Fraction(0)
    a %= b
    assert a > 0 and gcd(a, b) == 1
    return (-Fraction(1, 4) + (Fraction(a, b)+Fraction(b, a)
            +Fraction(1, a*b))/12 - dedekind_reciprocity(b, a))


def lattice_rho(p, q, u):
    """Count actual points of the Casson-Gordon triangle, including edges.

    The triangle has vertices (0,0), (k,0), (k,k*q/p), where k*q=u mod p.
    Interior, open-edge, nonorigin vertex weights are 1, 1/2, 1/4.
    k=0 is handled separately: the trivial rho is zero.
    """
    if u == 0:
        return Fraction(0)
    k = (u*pow(q, -1, p)) % p
    weighted = Fraction(0)
    for x in range(k+1):
        for y in range((k*q)//p+1):
            if p*y > q*x or (x == 0 and y == 0):
                continue
            if x == k and (y == 0 or p*y == q*k):
                weighted += Fraction(1, 4)
            elif y == 0 or x == k or p*y == q*x:
                weighted += Fraction(1, 2)
            else:
                weighted += 1
    area = Fraction(q*k*k, 2*p)
    return 4*(weighted-area)


def independent_exact(key):
    p, q, u = key
    rational = -4*dedekind_reciprocity(q,p)+lattice_rho(p,q,u)
    return QQ(rational.numerator) / QQ(rational.denominator)


def canonical_key(p, q, u):
    inverse = pow(q, -1, p)
    if inverse < q:
        q, u = inverse, (u*inverse) % p
    return p, q, min(u, (-u) % p)


def control_checks():
    known = {(2,1,0):QQ(0), (2,1,1):QQ(0), (3,1,0):-QQ(2)/9,
             (3,1,1):QQ(1)/9, (5,1,0):-QQ(4)/5, (5,1,1):-QQ(1)/5,
             (5,1,2):QQ(3)/5, (3,2,1):-QQ(1)/9}
    assert dedekind_reciprocity(1, 3) == Fraction(1,18)
    assert lattice_rho(3,1,1) == Fraction(1,3)
    assert lattice_rho(5,1,2) == Fraction(7,5)
    for key, expected in known.items():
        ball = cotangent_eta(*key)
        assert ball.is_finite() and R(ball.rad()) < R(10)**(-130)
        assert ball.contains_exact(expected), (key, ball, expected)
        assert independent_exact(key) == expected, (key, independent_exact(key), expected)
    return {','.join(map(str,k)):str(v) for k,v in known.items()}


def main():
    controls = control_checks()  # Must pass before using either checker.
    generator = generate.LensEta()
    expected_keys = [tuple(int(params[k]) for k in generator.parameters)
                     for params in generator.enumerate()]
    stored_mode = '--stored' in sys.argv or os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1'
    verify_report = None
    if stored_mode:
        client = generate.client_from_environment()
        report = generator.verify(sample=None, client=client)
        assert report.ok, str(report)
        verify_report = str(report)
        doc = numberdb.table('T622', client=client)
        # The entries endpoint stores flat records; no Django database is needed.
        assert isinstance(doc['Numbers'], list)
        values = {tuple(int(row['params'][k]) for k in generator.parameters):
                  QQ(row['number']) for row in doc['Numbers']}
        assert list(values) == expected_keys
    else:
        values = {key:generate.eta_exact(*key) for key in expected_keys}
    assert len(values) == 637
    assert len(set(key[:2] for key in values)) == 87
    assert not exactness(values)
    assert not names_its_rings(str(Path(__file__).with_name('generate.py')))
    disagreements = agrees_with(values, independent_exact)
    assert not disagreements, disagreements[:10]

    ball_records = []
    max_radius = R(0)
    min_relative_bits = BITS
    symmetry_count = 0
    all_domain_count = 0
    q1_count = 0
    for key, value in values.items():
        p,q,u = key
        ball = cotangent_eta(p,q,u)
        assert ball.is_finite() and R(ball.rad()) < R(10)**(-130), (key, ball)
        assert ball.contains_exact(value), (key, ball, value)
        if value:
            assert ball.accuracy() >= 430, (key, ball.accuracy())
            min_relative_bits = min(min_relative_bits, int(ball.accuracy()))
        max_radius = max(max_radius, R(ball.rad()))
        ball_records.append({'p':p,'q':q,'u':u,'exact':str(value),
                             'cotangent_ball':str(ball), 'radius':str(ball.rad())})
        if q == 1:
            formula = -QQ((p-1)*(p-2))/QQ(3*p)
            if u: formula += QQ(2*u*(p-u))/QQ(p)-1
            assert formula == value
            q1_count += 1
    # Check all omitted labels as well, using stored values for reconstruction.
    for p in range(2, generate.MAX_P+1):
        for q in range(1, p):
            if gcd(p,q) != 1: continue
            for u in range(p):
                key = (p,q,u)
                value = values[canonical_key(*key)]
                assert generate.eta_exact(*key) == value
                assert independent_exact(key) == value
                assert values[canonical_key(p,q,(-u)%p)] == value
                assert values[canonical_key(p,p-q,u)] == -value
                r=pow(q,-1,p)
                assert values[canonical_key(p,r,(u*r)%p)] == value
                symmetry_count += 3
                all_domain_count += 1
            assert sum(values[canonical_key(p,q,u)] for u in range(p)) == 0
    evidence = {'table':'T622','mode':'stored' if stored_mode else 'local',
                'archived_skill_manifest':json.loads(Path(__file__).with_name('skill-manifest.json').read_text()),
                'sage_version':SAGE_VERSION,'python_version':sys.version,
                'working_bits':BITS,'controls':controls,'measure':measure(values),
                'selected_entries':len(values),'oriented_presentations':87,
                'independent_exact_comparisons':len(values),
                'ball_comparisons':len(values),'max_ball_radius':str(max_radius),
                'minimum_nonzero_relative_accuracy_bits':min_relative_bits,
                'full_domain_reconstruction_checks':all_domain_count,
                'symmetry_comparisons':symmetry_count,'homogeneous_formula_checks':q1_count,
                'character_sum_zero_checked':True,'verify_report':verify_report,
                'generator_files':{name:hashlib.sha256(Path(__file__).with_name(name).read_bytes()).hexdigest()
                                   for name in ['generate.py','checks.py','check.py','argument-selection.md']},
                'q_representatives':{str(p):sorted({q for pp,q,u in values if pp==p}) for p in range(2,21)},
                'records':ball_records}
    print('EVIDENCE_JSON')
    print(json.dumps(evidence,indent=2))


if __name__ == '__main__':
    main()
