#!/usr/bin/env python3
"""
Wrapping Criterion Verification for CRT Torus Embedding.

Tests the Sidon creation condition across random modulus choices
and set configurations for k = 2 and k >= 3.
"""
import math, random, itertools, hashlib, json
from typing import List, Tuple, Set

def egcd(a: int, b: int):
    if b == 0: return a, 1, 0
    g, x, y = egcd(b, a % b)
    return g, y, x - (a // b) * y

def modinv(a: int, m: int) -> int:
    g, x, _ = egcd(a % m, m)
    assert g == 1, f"{a} not invertible mod {m}"
    return x % m

def crt_lift(r1: int, r2: int, L1: int, L2: int) -> int:
    """CRT lift: find x in [0, L1*L2) with x ≡ r1 mod L1, x ≡ r2 mod L2."""
    t = ((r2 - r1) * modinv(L1, L2)) % L2
    return r1 + t * L1

def crt_lift_k(residues: List[int], moduli: List[int]) -> int:
    """CRT lift for k moduli via iterative Garner-like approach."""
    x = residues[0]
    m = moduli[0]
    for i in range(1, len(moduli)):
        t = ((residues[i] - x) * modinv(m, moduli[i])) % moduli[i]
        x += t * m
        m *= moduli[i]
    return x

def f_k(a: int, S: int, moduli: List[int]) -> int:
    """F(a) for k-modulus embedding: axis 1 = a mod L1, others = (S-a) mod Li."""
    residues = [a % moduli[0]] + [(S - a) % Li for Li in moduli[1:]]
    return crt_lift_k(residues, moduli)

def is_sidon(X: List[int]) -> bool:
    """Check Sidon property (all pairwise sums distinct)."""
    sums = set()
    for i in range(len(X)):
        for j in range(i, len(X)):
            s = X[i] + X[j]
            if s in sums: return False
            sums.add(s)
    return True

def sum_collisions(X: List[int]) -> List[Tuple[Tuple[int,int],Tuple[int,int]]]:
    """Return all sum collisions [(a,b),(c,d)] with a+b = c+d, ordered."""
    sum_map = {}
    collisions = []
    for i in range(len(X)):
        for j in range(i, len(X)):
            s = X[i] + X[j]
            if s in sum_map:
                for pair in sum_map[s]:
                    collisions.append((pair, (i, j)))
            sum_map.setdefault(s, []).append((i, j))
    return collisions

def wrapping_criterion(a, b, c, d, S, moduli):
    """Check if two colliding pairs wrap the modulus boundary differently."""
    M = 1
    for Li in moduli: M *= Li
    Fa_sum = f_k(a, S, moduli) + f_k(b, S, moduli)
    Fc_sum = f_k(c, S, moduli) + f_k(d, S, moduli)
    wrap_ab = Fa_sum >= M
    wrap_cd = Fc_sum >= M
    return wrap_ab != wrap_cd, Fa_sum, Fc_sum, M

def test_2_modulus():
    """Test the known Sidon example and random cases for k=2."""
    print("=== k=2 Tests ===")
    tests = [
        # (A, S, L1, L2, description)
        ([1,2,5,6], 7, 3, 4, "Sidon creation example"),
        ([1,2,5,6], 100, 3, 4, "S changed, same A"),
        ([1,3,5,7], 8, 3, 5, "Symmetric set, odd"),
        ([0,2,4,6], 6, 5, 7, "Even set"),
        ([1,4,6,9], 10, 7, 11, "Random set"),
        ([0,1,3,4], 4, 3, 5, "Small set"),
        ([2,5,7,10], 12, 5, 7, "Medium set"),
        ([0,3,5,8,10,13], 13, 5, 8, "6-element set"),
    ]
    for A, S, L1, L2, desc in tests:
        moduli = [L1, L2]
        M = L1 * L2
        A_sidon = is_sidon(A)
        FA = [f_k(a, S, moduli) for a in A]
        FA_sidon = is_sidon(FA)
        collisions = sum_collisions(A)
        wrapped = []
        for (i,j),(p,q) in collisions:
            a,b,c,d = A[i],A[j],A[p],A[q]
            diff, s1, s2, _ = wrapping_criterion(a, b, c, d, S, moduli)
            wrapped.append((a,b,c,d,s1,s2,diff))
        status = "OK" if FA_sidon else "FAIL"
        print(f"  {desc:30s} A_sidon={A_sidon} FA_sidon={FA_sidon} |A|={len(A)} M={M} coll={len(collisions)} wrap={len(wrapped)}")

def test_3_modulus():
    """Test with k=3 moduli."""
    print("\n=== k=3 Tests ===")
    tests = [
        ([1,2,5,6], 7, [3,4,5]),
        ([1,2,5,6], 7, [3,5,7]),
        ([0,1,3,4], 4, [3,5,7]),
        ([0,2,4,6,8,10], 10, [5,7,11]),
        ([1,4,6,9,11,14], 15, [7,11,13]),
    ]
    for A, S, moduli in tests:
        M = 1
        for Li in moduli: M *= Li
        FA = [f_k(a, S, moduli) for a in A]
        FA_sidon = is_sidon(FA)
        collisions = sum_collisions(A)
        wrapped = []
        for (i,j),(p,q) in collisions:
            a,b,c,d = A[i],A[j],A[p],A[q]
            diff, s1, s2, _ = wrapping_criterion(a, b, c, d, S, moduli)
            wrapped.append(diff)
        print(f"  moduli={moduli} |A|={len(A)} M={M} A_sidon={is_sidon(A)} FA_sidon={FA_sidon} coll={len(collisions)} wraps={wrapped.count(True)}")

def test_k_random():
    """Test with randomly generated parameters for various k."""
    print("\n=== Random k >= 2 tests ===")
    random.seed(42)
    primes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53]
    for k in [2,3,4,6,8]:
        for trial in range(20):
            moduli = random.sample(primes, k)
            # Need all coprime — fine with distinct primes
            maxA = random.randint(5, 30)
            A = sorted(random.sample(range(0, maxA), min(maxA, random.randint(4, 8))))
            S = random.randint(maxA, 2*maxA)
            # Quick closure check: ensure A is S-closed (may not be — that's deliberate)
            M = 1
            for Li in moduli: M *= Li
            FA = [f_k(a, S, moduli) for a in A]
            FA_sidon = is_sidon(FA)
            A_sidon = is_sidon(A)
            collisions = sum_collisions(A)
            wrapped_count = 0
            for (i,j),(p,q) in collisions:
                a,b,c,d = A[i],A[j],A[p],A[q]
                diff, _, _, _ = wrapping_criterion(a, b, c, d, S, moduli)
                if diff: wrapped_count += 1
            if collisions or not FA_sidon:
                print(f"  k={k} |A|={len(A)} M={M} A_sidon={A_sidon} FA_sidon={FA_sidon} coll={len(collisions)} wraps={wrapped_count}")

def pairwise_sums(X):
    """Return the set of all pairwise sums of X."""
    sums = {}
    for i in range(len(X)):
        for j in range(i, len(X)):
            s = X[i] + X[j]
            sums.setdefault(s, []).append((i,j))
    return sums

def m_difference_condition(A, M):
    """
    Condition (b): no two distinct pairwise sums of A differ by exactly M.
    Returns (holds: bool, violators: list).
    """
    sums = pairwise_sums(A)
    sum_vals = list(sums.keys())
    violators = []
    for i in range(len(sum_vals)):
        for j in range(i+1, len(sum_vals)):
            if abs(sum_vals[i] - sum_vals[j]) == M:
                violators.append((sum_vals[i], sum_vals[j],
                                  sums[sum_vals[i]], sums[sum_vals[j]]))
    return len(violators) == 0, violators

def wrapping_condition(A, S, moduli):
    """
    Condition (a): for every sum collision in A, the pairs wrap M differently.
    Returns (holds: bool, unresolved: list).
    """
    M = 1
    for Li in moduli: M *= Li
    collisions = sum_collisions(A)
    unresolved = []
    for (i,j),(p,q) in collisions:
        a,b,c,d = A[i],A[j],A[p],A[q]
        diff, s1, s2, _ = wrapping_criterion(a,b,c,d,S,moduli)
        if not diff:
            unresolved.append(((a,b,c,d),(s1,s2)))
    return len(unresolved) == 0, unresolved

def certify_sidon_creation(A, S, moduli, verbose=False):
    """
    Certify whether F(A) is guaranteed Sidon.
    Returns (guaranteed: bool, FA: list, reason: str).
    """
    M = 1
    for Li in moduli: M *= Li
    FA = [f_k(a, S, moduli) for a in A]
    FA_sidon = is_sidon(FA)
    
    # Check injection regime
    if M <= max(A):
        return False, FA, f"Aliasing regime (M={M} <= max(A)={max(A)}), F not injective"
    
    # Check condition (a): wrapping
    wrap_ok, unresolved = wrapping_condition(A, S, moduli)
    
    # Check condition (b): M-difference
    mdiff_ok, violators = m_difference_condition(A, M)
    
    if wrap_ok and mdiff_ok:
        return True, FA, "Guaranteed Sidon (both conditions satisfied)"
    elif not wrap_ok:
        return False, FA, f"Wrapping criterion fails for {len(unresolved)} collision(s)"
    elif not mdiff_ok:
        return False, FA, f"M-difference condition fails ({len(violators)} violator(s))"
    else:
        return False, FA, "Unknown failure"

def verify_complete_theorem():
    """Verify the complete Sidon theorem (both conditions)."""
    print("\n=== Complete Theorem Verification ===")
    random.seed(456)
    primes = [2,3,5,7,11,13,17,19,23,29,31,37]
    passed = 0
    failed = 0
    for trial in range(2000):
        k = random.randint(2, 5)
        moduli = random.sample(primes, k)
        M = 1
        for Li in moduli: M *= Li
        n = random.randint(3, 10)
        maxA = random.randint(3, 20)
        A = sorted(random.sample(range(maxA+1), min(n, maxA+1)))
        S = random.randint(maxA, 2*maxA)
        
        # Only test in the injective regime (M > max(A))
        if M <= max(A):
            continue
        
        guaranteed, FA, reason = certify_sidon_creation(A, S, moduli)
        FA_sidon = is_sidon(FA)
        
        if guaranteed and FA_sidon:
            passed += 1
        elif not guaranteed and not FA_sidon:
            passed += 1
        else:
            print(f"  COUNTEREXAMPLE: guaranteed={guaranteed} FA_sidon={FA_sidon}")
            print(f"    k={k} moduli={moduli} M={M} A={A} S={S} FA={FA}")
            print(f"    reason={reason}")
            failed += 1
            if failed >= 5: break
    print(f"  Passed: {passed} / {passed+failed}")
    
    # Also certify the Sidon creation example
    print()
    A_ex = [1,2,5,6]
    S_ex = 7
    mod_ex = [3,4]
    g, FA, r = certify_sidon_creation(A_ex, S_ex, mod_ex, verbose=True)
    print(f"  Sidon example: guaranteed={g}, FA={FA}")
    print(f"    Reason: {r}")

if __name__ == "__main__":
    test_2_modulus()
    test_3_modulus()
    test_k_random()
    verify_complete_theorem()
