# -*- coding: utf-8 -*-
"""
AUDIT 3c — integrate the non-automorphism deformation direction.

Question: is the 6th kernel direction a genuine MODULUS (a 1-parameter family
of counterexamples through Alpoge's map) or first-order noise?

Method (all exact):
  1. identify the mystery direction d = ker M minus automorphism span; check
     whether it moves the SLOPE coefficients (it should — fixed-slope forge was rigid).
  2. second-order obstruction: is M*theta2 = -Q2(d) solvable?
  3. finite integration, staged exactly:
       z^2 layer  -> slope variety Sigma (quadratic in 9 slope coeffs)
       z^1 layer  -> linear in P,Q,R given slope
       z^0 layer  -> remaining equations + c
     with gauge pins (s10=1, s30=-1) and a driving pin s_j = value+t.
  4. verify any found map end-to-end: det == const != 0, fiber degree, and
     root-multiplicity invariant of S1 (Alpoge: triple root) to certify NEW.
"""
import sympy as sp
import json, time

t0 = time.time()
x, y, z, t = sp.symbols('x y z t')
a = x*y
ps  = sp.symbols('p0:4');  s1s = sp.symbols('s10:14')
qs  = sp.symbols('q0:3');  s2s = sp.symbols('s20:23')
rs  = sp.symbols('r0:3');  s3s = sp.symbols('s30:32')
c   = sp.Symbol('c')
TH  = list(ps)+list(s1s)+list(qs)+list(s2s)+list(rs)+list(s3s)+[c]
SL  = list(s1s)+list(s2s)+list(s3s)
PQR = list(ps)+list(qs)+list(rs)

def poly(cs): return sum(co*a**i for i, co in enumerate(cs))
F1s = y**2*poly(ps) + z*poly(s1s)
F2s = y + x*y**2*poly(qs) + x*z*poly(s2s)
F3s = x*poly(rs) + x**3*z*poly(s3s)
D  = sp.expand(sp.Matrix([F1s,F2s,F3s]).jacobian([x,y,z]).det() - c)

th0 = dict(zip(ps,[4,7,3,0])); th0.update(zip(s1s,[1,3,3,1]))
th0.update(zip(qs,[12,9,0]));  th0.update(zip(s2s,[3,6,3]))
th0.update(zip(rs,[2,-3,0]));  th0.update(zip(s3s,[-1,0])); th0[c] = -2

K = sp.Poly(D, x, y, z).coeffs()
M = sp.Matrix([[sp.diff(k, v).subs(th0) for v in TH] for k in K])
ns = M.nullspace()

# trivial span (5 independent from audit 3b)
e = sp.Symbol('epsilon')
F0 = [F1s.subs(th0), sp.expand(F2s.subs(th0)), F3s.subs(th0)]
def extract(Fs):
    out = {}
    specs = [ (Fs[0], [((j, j+2, 0), ps[j]) for j in range(4)] + [((j, j, 1), s1s[j]) for j in range(4)]),
              (sp.expand(Fs[1]-y), [((j+1, j+2, 0), qs[j]) for j in range(3)] + [((j+1, j, 1), s2s[j]) for j in range(3)]),
              (Fs[2], [((j+1, j, 0), rs[j]) for j in range(3)] + [((j+3, j, 1), s3s[j]) for j in range(2)]) ]
    for comp, slots in specs:
        p = sp.Poly(sp.expand(comp), x, y, z)
        table = {mono: co for mono, co in zip(p.monoms(), p.coeffs())}
        for mono, sym in slots:
            out[sym] = table.pop(mono, 0)
        if table: return None
    return out
lam = 1 + e
trivs = []
for Fe, cf in [([f.subs({x: lam*x}, simultaneous=True) for f in F0], -2*lam),
               ([f.subs({z: lam*z}, simultaneous=True) for f in F0], -2*lam),
               ([lam*F0[0], F0[1], F0[2]], -2*lam),
               ([F0[0], F0[1], lam*F0[2]], -2*lam),
               ([f.subs({z: z + e*y**2}, simultaneous=True) for f in F0], sp.Integer(-2)+0*e)]:
    ex = extract([sp.expand(f) for f in Fe])
    trivs.append([sp.diff(ex.get(v_, 0), e).subs(e,0) if v_ != c else sp.diff(cf, e).subs(e,0) for v_ in TH])
T = sp.Matrix(trivs).T

# --- 1. mystery direction --------------------------------------------------
d = None
for n in ns:
    if T.row_join(sp.Matrix(n)).rank() > T.rank():
        # project out trivial components (exact least squares over QQ)
        Tt = T.T
        coef = (Tt*T).solve(Tt*sp.Matrix(n))
        d = sp.Matrix(n) - T*coef
        break
assert d is not None
d = d * sp.lcm([sp.denom(v) for v in d])   # clear denominators
d = d / sp.gcd([sp.numer(v) for v in d if v != 0])
moves = {str(TH[i]): str(d[i]) for i in range(len(TH)) if d[i] != 0}
slope_moves = any(TH[i] in SL and d[i] != 0 for i in range(len(TH)))
print(f"mystery direction (cleared): {moves}", flush=True)
print(f"[{'PASS' if slope_moves else 'INFO'}] direction moves SLOPE coefficients: {slope_moves} "
      f"(consistent with fixed-slope rigidity)", flush=True)

# --- 2. second-order obstruction ------------------------------------------
tt = sp.Symbol('tt')
sub_t = {TH[i]: th0[TH[i]] + tt*d[i] for i in range(len(TH))}
Q2 = sp.Matrix([sp.expand(k.subs(sub_t)).coeff(tt, 2) for k in K])
aug = M.row_join(-Q2)
solvable = (aug.rank() == M.rank())
print(f"[{'PASS' if solvable else 'OBSTRUCTED'}] second-order equation M*theta2 = -Q2(d) is "
      f"{'solvable: unobstructed to 2nd order' if solvable else 'UNSOLVABLE: direction obstructed'}", flush=True)

# --- 3. finite integration -------------------------------------------------
# choose driving coordinate: first slope coordinate moved by d
drive = next(TH[i] for i in range(len(TH)) if TH[i] in SL and d[i] != 0)
print(f"driving coordinate: {drive}", flush=True)

def solve_at(tval):
    pins = {s1s[0]: 1, s3s[0]: -1, drive: th0[drive] + tval}
    # z-layers
    Dp = sp.Poly(D.subs(pins), z)
    layers = {i: sp.Poly(Dp.coeff_monomial(z**i) if i else Dp.coeff_monomial(1), x, y).coeffs()
              for i in (2, 1, 0)}
    # Sigma: z^2 layer -> slope unknowns only
    slope_un = [s for s in SL if s not in pins]
    sigma = sp.solve([sp.expand(q) for q in layers[2]], slope_un, dict=True)
    results = []
    for sg in sigma:
        rem_slope = [s for s in slope_un if s not in sg]
        # z^1 layer: linear in PQR
        eq1 = [sp.expand(q.subs(sg)) for q in layers[1]]
        lin = sp.linsolve(eq1, PQR)
        if lin == sp.EmptySet: continue
        pq = dict(zip(PQR, list(lin)[0]))
        free_pqr = sorted({s for v_ in pq.values() for s in v_.free_symbols if s in PQR}, key=str)
        # z^0 layer
        eq0 = [sp.expand(q.subs(sg).subs(pq)) for q in layers[0]]
        # constant term equation contains c; separate
        unknowns = rem_slope + free_pqr + [c]
        sol0 = sp.solve(eq0, unknowns, dict=True)
        for s0 in sol0:
            full = dict(pins); full.update({k_: sp.expand(v_.subs(s0)) for k_, v_ in sg.items()})
            full.update({k_: sp.expand(v_.subs(sg).subs(s0)) for k_, v_ in pq.items()})
            full.update(s0)
            for sym in TH:
                full.setdefault(sym, th0[sym] if sym in pins else full.get(sym, 0))
            cv = full.get(c, s0.get(c, 0))
            if cv == 0 or (hasattr(cv, 'free_symbols') and cv.free_symbols): continue
            results.append(full)
    return results

def certify(full, tval):
    Fi = [sp.expand(F1s.subs(full)), sp.expand(F2s.subs(full)), sp.expand(F3s.subs(full))]
    dJ = sp.expand(sp.Matrix(Fi).jacobian([x,y,z]).det())
    okK = dJ.free_symbols == set() and dJ != 0
    if not okK: return None
    # fiber degree at a random target (groebner quotient dim)
    t1, t2, t3 = sp.Rational(7,3), sp.Rational(-5,4), sp.Rational(9,5)
    zs = sp.solve(Fi[2] - t3, z)
    if len(zs) != 1: return None
    g1 = sp.expand(sp.numer(sp.together(Fi[0].subs(z, zs[0]) - t1)))
    g2 = sp.expand(sp.numer(sp.together(Fi[1].subs(z, zs[0]) - t2)))
    G = sp.groebner([g1, g2], x, y, order='grevlex')
    lms = [sp.Poly(p_, x, y).monoms(order='grevlex')[0] for p_ in G.polys]
    bx = min((m_[0] for m_ in lms if m_[1]==0), default=None)
    by = min((m_[1] for m_ in lms if m_[0]==0), default=None)
    if bx is None or by is None: return None
    nstd = sum(1 for i in range(bx) for j in range(by)
               if not any(i>=m_[0] and j>=m_[1] for m_ in lms))
    # invariant: root multiplicity of S1 cubic (Alpoge: (1+a)^3 triple root)
    S1p = sp.Poly(sum(co*sp.Symbol('A')**i for i, co in enumerate([full[s] for s in s1s])), sp.Symbol('A'))
    sq = sp.degree(sp.gcd(S1p, S1p.diff(sp.Symbol('A'))).as_expr(), sp.Symbol('A')) if S1p.degree() > 0 else 0
    return {"t": str(tval), "det": str(dJ), "fiber_points": nstd,
            "S1_coeffs": [str(full[s]) for s in s1s],
            "S1_gcd_with_deriv_degree": int(sq),
            "alpoge_S1_gcd_degree": 2,
            "is_new_shape": bool(sq < 2),
            "coeffs": {str(k_): str(v_) for k_, v_ in full.items() if k_ in TH}}

out = {"mystery_direction": moves, "slope_moving": bool(slope_moves),
       "second_order_unobstructed": bool(solvable), "families": []}
for tval in (sp.Rational(1,2), sp.Integer(1), sp.Integer(-1)):
    try:
        sols = solve_at(tval)
        print(f"t = {tval}: {len(sols)} exact Keller solution(s) found", flush=True)
        for fu in sols[:3]:
            cert = certify(fu, tval)
            if cert:
                tag = "NEW-SHAPE" if cert["is_new_shape"] else "same-S1-multiplicity"
                print(f"    det={cert['det']} fiber={cert['fiber_points']} S1={cert['S1_coeffs']} [{tag}]", flush=True)
                out["families"].append(cert)
    except Exception as ex:
        print(f"t = {tval}: solver exception: {type(ex).__name__}: {ex}", flush=True)

out["elapsed"] = round(time.time()-t0, 2)
json.dump(out, open('post-jc-program/audit/audit3c.json','w'), indent=2)
print(f"\nartifact: audit3c.json ({out['elapsed']}s)", flush=True)
