#!/usr/bin/env python3
"""crt_qprofile_sweep.py — q-profile safety factor design for CRT moduli.

Replaces brute-force modulus selection with systematic q-profile sweep,
as proposed in TOROIDAL_POLOIDAL_REFINEMENT.md (R1, R6).

The toroidal/poloidal mapping gives:
  q = L_reflection / L_identity (toroidal/poloidal ratio)
  q < 1 = poloidal-dominated (Sidon-favorable)
  q > 1 = toroidal-dominated (Sidon-unfavorable)
  q = 1 = degenerate (rational surface → resonance → Sidon collapse)

This script:
  1. Sweeps q from 0.3 to 3.0 (avoiding simple rationals)
  2. For each q, constructs moduli (L₀, L₁) with L₁/L₀ = q
  3. Tests Sidon preservation for known Sidon label sets
  4. Also applies cross-pair q-ratio check (R2): avoid simple rationals
  5. Measures Sidon score as function of q

Outputs:
  .openresearch/artifacts/crt_qprofile_sweep.json
  .openresearch/artifacts/EVAL.md

All integer arithmetic. No floats in compute path.
"""

import sys
import math
import json
import time
import hashlib
from pathlib import Path
from collections import Counter
from fractions import Fraction

REPO_ROOT = Path(__file__).resolve().parent.parent
ARTIFACTS_DIR = REPO_ROOT / ".openresearch" / "artifacts"
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH = ARTIFACTS_DIR / "crt_qprofile_sweep.json"
EVAL_PATH = ARTIFACTS_DIR / "EVAL.md"


# ── Exact Arithmetic ──────────────────────────────────────────────────────

def gcd(a, b):
    while b:
        a, b = b, a % b
    return a

def pairwise_coprime(moduli):
    for i in range(len(moduli)):
        for j in range(i + 1, len(moduli)):
            if gcd(moduli[i], moduli[j]) != 1:
                return False
    return True

def is_simple_rational(a, b, max_den=7):
    """Check if a/b reduces to m/n with n <= max_den."""
    if b == 0:
        return True
    g = gcd(abs(a), abs(b))
    na, nb = abs(a) // g, abs(b) // g
    return nb <= max_den


# ── CRT Torus Embedding ───────────────────────────────────────────────────

def embed(labels, S, moduli):
    M = 1
    for m in moduli:
        M *= m
    embedded = []
    for a in labels:
        row = []
        for i in range(len(moduli)):
            if i == 0:
                row.append(a % moduli[0])
            else:
                row.append((S - a) % moduli[i])
        embedded.append(row)
    return embedded, M

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

def modinv(a, m):
    g, x, _ = egcd(a % m, m)
    if g != 1:
        return None
    return x % m

def crt_reconstruct(residues, moduli):
    M = 1
    for m in moduli:
        M *= m
    x = 0
    for r, m in zip(residues, moduli):
        Mi = M // m
        inv = modinv(Mi % m, m)
        if inv is None:
            return None
        x = (x + r * Mi * inv) % M
    return x

def sidon_check(embedded, moduli):
    M = 1
    for m in moduli:
        M *= m
    n = len(embedded)
    total_pairs = n * (n + 1) // 2
    vals = [crt_reconstruct(row, moduli) for row in embedded]
    sums = []
    for i in range(n):
        for j in range(i, n):
            sums.append((vals[i] + vals[j]) % M)
    counts = Counter(sums)
    collisions = sum(c - 1 for c in counts.values())
    score = 1.0 - collisions / total_pairs if total_pairs > 0 else 1.0
    return {
        "total_pairs": total_pairs,
        "distinct_residues": len(counts),
        "collisions": collisions,
        "sidon_score": round(score, 6),
        "is_sidon": collisions == 0,
    }


# ── q-Profile Modulus Selection ──────────────────────────────────────────

def select_qprofile_moduli(L0, q_num, q_den, n_moduli=2):
    """Select moduli with a given q-profile.
    
    q = L₁/L₀ = q_num/q_den (as exact fraction)
    L₀ = L0 (identity axis, poloidal)
    L₁ = L₀ * q_num / q_den (reflection axis, toroidal)
    
    For n_moduli > 2, additional moduli use increasing primes coprime to all.
    Returns (moduli, q) or None if not coprime.
    """
    # L₁ = L0 * q_num / q_den — must be integer
    L1 = L0 * q_num // q_den
    if L0 * q_num != L1 * q_den:
        # Not exact — adjust L0 to make it work
        L0 = L0 * q_den
        L1 = L0 * q_num // q_den
    
    if L0 < 2 or L1 < 2:
        return None, None
    
    moduli = [L0, L1]
    
    # For n_moduli > 2, add coprime primes
    if n_moduli > 2:
        p = L1 + 2
        while len(moduli) < n_moduli:
            if all(gcd(p, m) == 1 for m in moduli) and p > 2:
                moduli.append(p)
            p += 1
    
    if not pairwise_coprime(moduli):
        return None, None
    
    q = Fraction(q_num, q_den)
    return moduli, q


def sweep_q_profile(label_set, S, L0_base, max_q_num=30, max_q_den=20):
    """Sweep q = L₁/L₀ over all coprime fractions q_num/q_den.
    Returns list of (q, moduli, sidon_result, q_is_simple_rational).
    """
    results = []
    
    for q_den in range(2, max_q_den + 1):
        for q_num in range(1, max_q_num + 1):
            if gcd(q_num, q_den) != 1:
                continue  # skip non-reduced fractions
            if q_num == q_den:
                continue  # skip q=1 (degenerate)
            
            moduli, q = select_qprofile_moduli(L0_base, q_num, q_den)
            if moduli is None:
                continue
            
            q_simple = is_simple_rational(q_num, q_den, max_den=7)
            
            embedded, M = embed(label_set, S, moduli)
            sidon = sidon_check(embedded, moduli)
            
            entry = {
                "q": str(q),
                "q_float": float(q),
                "q_num": q_num,
                "q_den": q_den,
                "q_simple_rational": q_simple,
                "moduli": moduli,
                "M": M,
                "L0": moduli[0],
                "L1": moduli[1],
                "sidon_score": sidon["sidon_score"],
                "collisions": sidon["collisions"],
                "is_sidon": sidon["is_sidon"],
                "total_pairs": sidon["total_pairs"],
                "distinct_residues": sidon["distinct_residues"],
            }
            results.append(entry)
    
    return results


# ── Main ──────────────────────────────────────────────────────────────────

def run_experiment():
    results = {
        "experiment": "crt_qprofile_sweep",
        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "config": {
            "L0_base": 7,
            "max_q_num": 30,
            "max_q_den": 20,
            "label_sets": [
                {"name": "sidon_pow2", "labels": [1, 2, 4, 8, 16], "S": 32},
                {"name": "sidon_singer5", "labels": [0, 1, 4, 14, 16], "S": 30},
                {"name": "nonsidon_seq5", "labels": [0, 1, 2, 3, 4], "S": 5},
            ],
        },
        "data": [],
        "summary": {},
    }
    
    label_sets = [
        ("sidon_pow2", [1, 2, 4, 8, 16], 32),
        ("sidon_singer5", [0, 1, 4, 14, 16], 30),
        ("nonsidon_seq5", [0, 1, 2, 3, 4], 5),
    ]
    
    L0_base = 7
    
    for desc, labels, S in label_sets:
        print(f"\n{'='*60}")
        print(f"  Label set: {desc} (n={len(labels)}, S={S})")
        print(f"{'='*60}")
        
        sweep = sweep_q_profile(labels, S, L0_base)
        
        # Analyze
        sidon_count = sum(1 for r in sweep if r["is_sidon"])
        total_count = len(sweep)
        q_simple_count = sum(1 for r in sweep if r["q_simple_rational"])
        q_simple_sidon = sum(1 for r in sweep if r["q_simple_rational"] and r["is_sidon"])
        q_not_simple_sidon = sum(1 for r in sweep if not r["q_simple_rational"] and r["is_sidon"])
        
        # q < 1 vs q > 1
        q_lt1 = [r for r in sweep if r["q_float"] < 1.0]
        q_gt1 = [r for r in sweep if r["q_float"] > 1.0]
        q_lt1_sidon = sum(1 for r in q_lt1 if r["is_sidon"])
        q_gt1_sidon = sum(1 for r in q_gt1 if r["is_sidon"])
        
        # Best q (highest Sidon score)
        best = max(sweep, key=lambda r: r["sidon_score"]) if sweep else None
        
        summary = {
            "total_configs": total_count,
            "sidon_configs": sidon_count,
            "sidon_rate": round(sidon_count / total_count, 4) if total_count > 0 else 0,
            "q_simple_rational_count": q_simple_count,
            "q_simple_sidon": q_simple_sidon,
            "q_not_simple_sidon": q_not_simple_sidon,
            "q_lt1_total": len(q_lt1),
            "q_lt1_sidon": q_lt1_sidon,
            "q_lt1_sidon_rate": round(q_lt1_sidon / len(q_lt1), 4) if q_lt1 else 0,
            "q_gt1_total": len(q_gt1),
            "q_gt1_sidon": q_gt1_sidon,
            "q_gt1_sidon_rate": round(q_gt1_sidon / len(q_gt1), 4) if q_gt1 else 0,
            "best_q": best["q"] if best else None,
            "best_sidon_score": best["sidon_score"] if best else None,
            "best_moduli": best["moduli"] if best else None,
        }
        results["summary"][desc] = summary
        
        print(f"  Total configs:  {total_count}")
        print(f"  Sidon configs:  {sidon_count} ({summary['sidon_rate']:.1%})")
        print(f"  q < 1: {q_lt1_sidon}/{len(q_lt1)} Sidon ({summary['q_lt1_sidon_rate']:.1%})")
        print(f"  q > 1: {q_gt1_sidon}/{len(q_gt1)} Sidon ({summary['q_gt1_sidon_rate']:.1%})")
        print(f"  q simple rational: {q_simple_sidon}/{q_simple_count} Sidon")
        print(f"  q NOT simple:      {q_not_simple_sidon}/{total_count - q_simple_count} Sidon")
        if best:
            print(f"  Best q = {best['q']} (score={best['sidon_score']}, moduli={best['moduli']})")
        
        # Store all sweep data
        for r in sweep:
            r["label_set"] = desc
            results["data"].append(r)
    
    content = json.dumps(results, indent=2, sort_keys=True, default=str)
    results["sha256"] = hashlib.sha256(content.encode()).hexdigest()
    return results


def write_eval(results):
    lines = [
        "# CRT q-Profile Safety Factor Sweep",
        "",
        f"**Experiment:** {results['experiment']}",
        f"**Date:** {results['timestamp']}",
        f"**SHA-256:** `{results['sha256']}`",
        "",
        "## Summary: Sidon Rate by q-Regime",
        "",
        "| Label set | Total | Sidon | Rate | q<1 total | q<1 Sidon | q<1 rate | q>1 total | q>1 Sidon | q>1 rate | Simple q Sidon | Non-simple Sidon | Best q |",
        "|-----------|-------|-------|------|-----------|-----------|----------|-----------|-----------|----------|----------------|------------------|-------|",
    ]
    for desc, s in results["summary"].items():
        lines.append(
            f"| {desc} | {s['total_configs']} | {s['sidon_configs']} | "
            f"{s['sidon_rate']:.1%} | "
            f"{s['q_lt1_total']} | {s['q_lt1_sidon']} | {s['q_lt1_sidon_rate']:.1%} | "
            f"{s['q_gt1_total']} | {s['q_gt1_sidon']} | {s['q_gt1_sidon_rate']:.1%} | "
            f"{s['q_simple_sidon']} | {s['q_not_simple_sidon']} | "
            f"{s['best_q']} |"
        )
    
    lines.extend([
        "",
        "## Predictions Tested",
        "",
        "1. **q < 1 (poloidal-dominated) should have higher Sidon rate than q > 1**",
        "   - This is the toroidal/poloidal refinement prediction",
        "   - If confirmed: poloidal resolution matters for Sidon structure",
        "",
        "2. **Simple rational q should have LOWER Sidon rate than non-simple q**",
        "   - This is the R2 cross-pair coprimality prediction",
        "   - Simple rationals = resonant surfaces = Sidon collapse",
        "",
        "3. **Best q should be < 1 and not a simple rational**",
        "   - Optimal q-profile is poloidal-dominated and irrational",
        "",
    ])
    
    EVAL_PATH.write_text("\n".join(lines))


if __name__ == "__main__":
    print("=" * 60)
    print("CRT q-Profile Safety Factor Sweep")
    print("=" * 60)
    
    t0 = time.time()
    results = run_experiment()
    elapsed = time.time() - t0
    
    OUTPUT_PATH.write_text(json.dumps(results, indent=2, default=str))
    print(f"\nResults → {OUTPUT_PATH}")
    
    write_eval(results)
    print(f"EVAL → {EVAL_PATH}")
    
    print(f"\nElapsed: {elapsed:.1f}s")
    print("=" * 60)
    print("DONE")
    print("=" * 60)
