# Reversible coordinate preconditioning (CLOSE to prior work; no novelty claim). Deterministic host-side layer.
# Arms: N = fixed conventional normalization (control), S = min-J selector over a small whitelist (identity, N, shifts, scalings).
# Every accepted transform is verified exactly with SymPy; a failed check raises (pre-registered stop rule).
import re, json, sys, itertools
import sympy as sp
from sympy.parsing.sympy_parser import parse_expr, standard_transformations, implicit_multiplication_application, convert_xor
from tokenizers import Tokenizer
TOK = None
def ntok(s):
    global TOK
    if TOK is None: import os; TOK = Tokenizer.from_file(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'qwen25_tokenizer.json'))
    return len(TOK.encode(s).ids)
LAM, MU, NU = 1.0, 1.0, 1.0   # frozen a priori (not tuned on test data)
MAXC = 16
SPAN = re.compile(r'\$\$(.+?)\$\$|\$(.+?)\$|\\\[(.+?)\\\]|\\\((.+?)\\\)', re.S)
OKCH = re.compile(r'^[0-9a-z+\-*/^(){}=. ]+$')
TR = standard_transformations + (implicit_multiplication_application, convert_xor)
def frac(s):
    while True:
        m = re.search(r'\\[dt]?frac\s*\{([^{}]*)\}\s*\{([^{}]*)\}', s)
        if not m: return s
        s = s[:m.start()] + '((' + m.group(1) + ')/(' + m.group(2) + '))' + s[m.end():]
def latex_side(t):
    t = t.replace('\\left', '').replace('\\right', '').replace('\\cdot', '*').replace('\\,', ' ').replace('\\!', '')
    t = frac(t).replace('{', '(').replace('}', ')')
    if '\\' in t or not OKCH.match(t + ' '): return None
    if re.search(r'[a-z]{2,}', t.replace(' ', '')) and not re.fullmatch(r'[0-9a-z+\-*/^(). ]+', t): return None
    if re.search(r'\d\.\d', t): return None
    try:
        e = parse_expr(t, transformations=TR, local_dict={c: sp.Symbol(c) for c in 'abcdefghijklmnopqrstuvwxyz'})
    except Exception: return None
    return e
def parse_span(s):
    s = s.strip().rstrip('.,')
    if s.count('=') > 1 or any(r in s for r in ['<', '>', '\\le', '\\ge', '\\ne']): return None
    if re.search(r'[a-z]\s*\(', re.sub(r'^\s*[a-z]\s*\(\s*[a-z]\s*\)\s*=', '', s)): return None  # function application, not multiplication
    if re.fullmatch(r'\s*[a-z]\s*=\s*-?[0-9]+\s*', s): return None  # bare assignment, nothing to precondition
    fdef = re.match(r'^\s*([a-z])\s*\(\s*([a-z])\s*\)\s*=(.*)$', s)
    if fdef:
        rhs = latex_side(fdef.group(3));
        if rhs is None: return None
        return ('fdef', fdef.group(1), rhs)
    if '=' in s:
        L, R = s.split('=')
        l, r = latex_side(L), latex_side(R)
        if l is None or r is None: return None
        return ('eq', None, l - r)
    e = latex_side(s)
    return None if e is None else ('expr', None, e)
def usable(kind, e):
    if not isinstance(e, sp.Expr): return None
    fs = sorted(e.free_symbols, key=str)
    if len(fs) != 1: return None
    x = fs[0]
    try: P = sp.Poly(sp.expand(e), x)
    except Exception: return None
    if not all(c.is_rational for c in P.all_coeffs()): return None
    if not (1 <= P.degree() <= 4) or len(P.terms()) < 2: return None
    if kind == 'expr' and P.degree() < 2: return None
    return x, P
def tex(e): return sp.latex(e)
def W(e): return max([len(a.args) for a in sp.preorder_traversal(e)] + [0])
def D(e):
    return 0 if not e.args else 1 + max(D(a) for a in e.args)
def normalize(kind, x, e):
    """Fixed conventional normalization: expand; for equations move all to one side, clear denominators, divide by content, positive leading coeff."""
    if kind == 'eq':
        P = sp.Poly(sp.expand(e), x); cs = P.all_coeffs()
        den = sp.ilcm(*[sp.fraction(c)[1] for c in cs]); P = P * den
        g = sp.igcd(*[int(c) for c in P.all_coeffs()]); P = P.quo_ground(g)
        if P.LC() < 0: P = -P
        return P.as_expr()
    return sp.expand(e)
def candidates(kind, x, e):
    """Whitelist: identity, N, affine x = a*u + b with b from centering (exact and rounded) and a from small coefficient-derived scalings."""
    u = sp.Symbol('u' if str(x) != 'u' else 'v')
    base = normalize(kind, x, e); P = sp.Poly(base, x); n = P.degree(); cs = P.all_coeffs()
    h = -cs[1] / (n * cs[0]) if n >= 2 else (-cs[1] / cs[0] if kind == 'eq' else 0)
    shifts = [0]
    for b in [h, sp.Integer(round(h))]:
        if b != 0 and b not in shifts: shifts.append(sp.nsimplify(b))
    scales = [1]
    if kind == 'eq':
        for a in [2, 3, 5, 10]:
            Q = sp.Poly(sp.expand(base.subs(x, a * u)), u)
            if sp.igcd(*[int(c) for c in Q.all_coeffs()]) > 1: scales.append(a)
    out = [('identity', None, e, x), ('normalize', None, base, x)]
    for b, a in itertools.product(shifts, scales):
        if b == 0 and a == 1: continue
        sub = a * u + b; t = sp.expand(base.subs(x, sub))
        if kind == 'eq':
            Q = sp.Poly(t, u); cs2 = Q.all_coeffs()
            den = sp.ilcm(*[sp.fraction(c)[1] for c in cs2]); Q = Q * den
            g = sp.igcd(*[int(c) for c in Q.all_coeffs()]); Q = Q.quo_ground(g)
            if Q.LC() < 0: Q = -Q
            t = Q.as_expr()
        out.append((f'affine a={a} b={b}', (x, sub), t, u))
        if len(out) >= MAXC: break
    return out
def verify(kind, x, e, c):
    name, m, t, v = c
    if m is None:
        r = sp.simplify(t - e) if kind != 'eq' else sp.simplify(sp.Poly(sp.expand(e), x).monic().as_expr() - sp.Poly(sp.expand(t), x).monic().as_expr())
        return r == 0
    xs, sub = m
    # inverse: u = (x - b)/a ; transformed in u, substitute back, must equal original (expr) or a nonzero constant multiple (eq)
    back = sp.expand(t.subs(v, sp.solve(sp.Eq(xs, sub), v)[0]))
    if kind == 'eq':
        return sp.simplify(sp.Poly(back, xs).monic().as_expr() - sp.Poly(sp.expand(e), xs).monic().as_expr()) == 0
    return sp.simplify(back - e) == 0
def render(kind, name, fn, c, orig_src):
    _, m, t, v = c
    if kind == 'eq': body = f'{tex(t)} = 0' if name != 'identity' else orig_src
    elif kind == 'fdef': body = f'{fn}({v}) = {tex(t)}' if m is None else f'{tex(t)}'
    else: body = tex(t) if name != 'identity' else orig_src
    if m is None: return f'${body}$'
    xs, sub = m
    if kind == 'fdef': return f'${tex(t)}$ (substituting ${xs} = {tex(sub)}$)'
    return f'${body}$ (substituting ${xs} = {tex(sub)}$)'
def cost(rep, c):
    _, m, t, v = c
    inv = 0 if m is None else ntok(f'{tex(m[0])} = {tex(m[1])}')
    return ntok(rep) + LAM * W(t) + MU * D(t) + NU * inv
def process(problem):
    spans = []
    for mm in SPAN.finditer(problem):
        src = next(g for g in mm.groups() if g is not None)
        p = parse_span(src)
        if not p: continue
        kind, fn, e = p
        us = usable('eq' if kind == 'eq' else 'expr', e)
        if not us: continue
        x, P = us
        k2 = 'eq' if kind == 'eq' else 'expr'
        cands = candidates(k2, x, e)
        for c in cands:
            if not verify(k2, x, e, c):
                raise RuntimeError(f'NON-EQUIVALENT TRANSFORM {c[0]} on {src!r}')
        reps = []
        for c in cands:
            rk = 'fdef' if kind == 'fdef' else k2
            rep = render(rk, c[0], fn, c, src) if c[0] != 'identity' else f'${src.strip()}$'
            reps.append((cost(rep, c), i := len(reps), c[0], rep))
        best = min(reps, key=lambda r: (r[0], r[1]))  # ties favor identity (index 0)
        nrm = reps[1]
        sq = lambda z: re.sub(r'[{}\s$.,]', '', z)
        if sq(nrm[3]).replace('with', '') == sq(reps[0][3]): nrm = reps[0]
        if best[2] == 'normalize' and nrm is reps[0]: best = reps[0]
        spans.append({'src': src.strip(), 'kind': kind, 'N': nrm[3] if nrm[3] != reps[0][3] else None,
                      'S': best[3] if best[2] != 'identity' else None, 'S_name': best[2],
                      'J': {r[2]: round(r[0], 2) for r in reps}})
    return spans
HDR = "\n\nAn exactly equivalent rewriting of some of the math above (checked by a computer algebra system; use it only if it helps):\n"
TAIL = "\nAnswer the original question, in terms of the original variables."
def arm_prompt(problem, spans, arm):
    lines = [f'- ${s["src"].rstrip(".,")}$ is equivalent to {s[arm]}' for s in spans if s[arm]]
    if not lines: return None
    return problem + HDR + "\n".join(lines) + TAIL
if __name__ == '__main__':
    rows = [json.loads(l) for l in open(sys.argv[1])]
    key = 'problem'; el = nN = nS = same = 0; sub = {}
    out = []
    for k, r in enumerate(rows):
        if r.get('subject', r.get('type', '')) in ('Geometry',): sp_ = []
        else: sp_ = process(r[key])
        pN, pS = arm_prompt(r[key], sp_, 'N'), arm_prompt(r[key], sp_, 'S')
        if pN or pS:
            el += 1; nN += pN is not None; nS += pS is not None; same += pN == pS
            sub[r.get('subject')] = sub.get(r.get('subject'), 0) + 1
        out.append({'item': k, 'spans': sp_, 'prompt_N': pN, 'prompt_S': pS})
    print('items', len(rows), 'eligible', el, 'N-differs', nN, 'S-differs', nS, 'N==S', same, sub)
    if len(sys.argv) > 2: json.dump(out, open(sys.argv[2], 'w'), indent=1)
