#!/usr/bin/env python3
"""semisymmetry_discriminator.py — Milestone §0: covariant semi-symmetry test.

Places the drift-perturbed Fisher–Rao geometry on the classical ladder
(docs/reviews/CONJECTURE_UPGRADE_ROADMAP.md §0):

    locally symmetric   ∇R = 0                    (Cartan)
    semisymmetric       R·R = 0, ∇R ≠ 0           (Szabó)
    pseudosymmetric     R·R = L·Q(g,R)            (Deszcz)

Geometry. The open simplex Δ_{m−1} = {p ∈ ℝ^m_{>0} : Σp = 1} in reduced
coordinates p₁..p_{m−1} (p_m = 1 − Σ) carries the Fisher–Rao metric

    g_ij = δ_ij / p_i + 1 / p_m            (positive-definite, (m−1, 0)).

0a  BASELINE: g has constant sectional curvature 1/4 (isometric to an
    orthant of the radius-2 sphere via p ↦ 2√p), hence ∇R ≡ 0 — fully
    symmetric, holonomy SO(m−1). Verified symbolically below.

0b  DRIFT PERTURBATION: BraidStateN.lean's rossbyDriftFromChirality
    assigns per-strand weights (left = +1, right = −1, scarred = +1/2,
    achiral = 0). Mean-centering w gives a tangent drift vector β
    (Σβ = 0). The *drift-flipped* metric

        g' = g − 2 (β♭ ⊗ β♭) / g(β, β),        β♭ = g β,

    is the reflection of g along β: it flips the sign of g exactly on
    span(β), so sig(g') = (1, m−2) with the TIME-LIKE direction = the
    drift direction. This is the concrete geometrization of "the
    Kelvin/Rossby directionality supplies the (1, m−2) signature"
    (§0-0b). β is extended as a constant field in the reduced chart —
    a modelling choice, stated explicitly.

0c  DISCRIMINATOR: compute R', ∇R', R'·R' and Q(g',R') for g' and test
    the ladder. Conventions (Kobayashi–Nomizu):

        R^ρ_{σμν} = ∂_μ Γ^ρ_{νσ} − ∂_ν Γ^ρ_{μσ}
                    + Γ^ρ_{μλ} Γ^λ_{νσ} − Γ^ρ_{νλ} Γ^λ_{μσ}
        R_{ρσμν} = g_{ρλ} R^λ_{σμν}

    Derivation action of an endomorphism A on a (0,4) tensor T:

        (A·T)_{ijkl} = −A^m_i T_{mjkl} − A^m_j T_{imkl}
                       −A^m_k T_{ijml} − A^m_l T_{ijkm}

    R·R uses A = R(∂a,∂b) (components A^m_i = R^m_{iab});
    Q(g,R) uses A = ∂a ∧_g ∂b (components A^m_i = δ^m_a g_{bi} − δ^m_b g_{ai}).
    Pseudosymmetry asks for a *function* L with R·R = L·Q(g,R).

All arithmetic exact (sympy Rational); point evaluations at rational
points. A nonzero tensor at one rational point is a PROOF of ≠ 0;
symbolic identities are proven over the whole chart.

Usage:
    python3 python/semisymmetry_discriminator.py            # m = 4 (Δ₃)
    python3 python/semisymmetry_discriminator.py --m 8      # Δ₇ (slow)
"""

from __future__ import annotations

import argparse
import itertools
import sys

import sympy as sp


# ── tensor machinery (exact, chart-based) ─────────────────────────────

def fisher_metric(m: int, ps):
    """Fisher–Rao on Δ_{m−1} in reduced coordinates."""
    pm = 1 - sum(ps)
    n = m - 1
    g = sp.zeros(n, n)
    for i in range(n):
        for j in range(n):
            g[i, j] = (1 / ps[i] if i == j else 0) + 1 / pm
    return sp.Matrix(g)


def christoffel(g: sp.Matrix, coords):
    n = len(coords)
    ginv = g.inv()
    Gamma = [[[sp.S.Zero] * n for _ in range(n)] for _ in range(n)]
    dg = [[[sp.diff(g[i, j], coords[k]) for k in range(n)]
           for j in range(n)] for i in range(n)]
    for l in range(n):
        for i in range(n):
            for j in range(n):
                s = sp.S.Zero
                for k in range(n):
                    s += ginv[l, k] * (dg[k][i][j] + dg[k][j][i] - dg[i][j][k])
                Gamma[l][i][j] = sp.together(s / 2)
    return Gamma


def riemann(g: sp.Matrix, Gamma, coords):
    """R^ρ_{σμν} (up) and R_{ρσμν} (down)."""
    n = len(coords)
    Rup = [[[[sp.S.Zero] * n for _ in range(n)] for _ in range(n)] for _ in range(n)]
    for rho in range(n):
        for sig in range(n):
            for mu in range(n):
                for nu in range(mu + 1, n):  # antisymmetry in (μ,ν)
                    term = sp.diff(Gamma[rho][nu][sig], coords[mu]) \
                         - sp.diff(Gamma[rho][mu][sig], coords[nu])
                    for lam in range(n):
                        term += Gamma[rho][mu][lam] * Gamma[lam][nu][sig] \
                              - Gamma[rho][nu][lam] * Gamma[lam][mu][sig]
                    term = sp.together(term)
                    Rup[rho][sig][mu][nu] = term
                    Rup[rho][sig][nu][mu] = -term
    Rdn = [[[[sp.S.Zero] * n for _ in range(n)] for _ in range(n)] for _ in range(n)]
    for rho in range(n):
        for sig in range(n):
            for mu in range(n):
                for nu in range(mu + 1, n):
                    s = sp.S.Zero
                    for lam in range(n):
                        s += g[rho, lam] * Rup[lam][sig][mu][nu]
                    s = sp.together(s)
                    Rdn[rho][sig][mu][nu] = s
                    Rdn[rho][sig][nu][mu] = -s
    return Rup, Rdn


def cov_deriv_riemann(Rdn, Gamma, coords):
    """∇_a R_{ijkl} — the local-symmetry obstruction."""
    n = len(coords)
    out = [[[[[sp.S.Zero] * n for _ in range(n)] for _ in range(n)]
            for _ in range(n)] for _ in range(n)]
    for a in range(n):
        for i in range(n):
            for j in range(n):
                for k in range(n):
                    for l in range(n):
                        s = sp.diff(Rdn[i][j][k][l], coords[a])
                        for mth in range(n):
                            s -= Gamma[mth][a][i] * Rdn[mth][j][k][l]
                            s -= Gamma[mth][a][j] * Rdn[i][mth][k][l]
                            s -= Gamma[mth][a][k] * Rdn[i][j][mth][l]
                            s -= Gamma[mth][a][l] * Rdn[i][j][k][mth]
                        out[a][i][j][k][l] = sp.together(s)
    return out


def derivation_action(A, Rdn, n):
    """(A·R)_{ijkl} for endomorphism components A[m][i] = A^m_i."""
    out = [[[[sp.S.Zero] * n for _ in range(n)] for _ in range(n)] for _ in range(n)]
    for i in range(n):
        for j in range(n):
            for k in range(n):
                for l in range(n):
                    s = sp.S.Zero
                    for mth in range(n):
                        s -= A[mth][i] * Rdn[mth][j][k][l]
                        s -= A[mth][j] * Rdn[i][mth][k][l]
                        s -= A[mth][k] * Rdn[i][j][mth][l]
                        s -= A[mth][l] * Rdn[i][j][k][mth]
                    out[i][j][k][l] = s
    return out


def rr_and_q(g, Rup, Rdn, n):
    """R·R and Q(g,R): 6-index tensors indexed [a][b] → (0,4) blocks."""
    RR = {}
    Q = {}
    for a in range(n):
        for b in range(a + 1, n):
            A_R = [[Rup[mth][i][a][b] for i in range(n)] for mth in range(n)]
            A_w = [[(g[b, i] if mth == a else 0) - (g[a, i] if mth == b else 0)
                    for i in range(n)] for mth in range(n)]
            RR[(a, b)] = derivation_action(A_R, Rdn, n)
            Q[(a, b)] = derivation_action(A_w, Rdn, n)
    return RR, Q


# ── evaluation helpers ────────────────────────────────────────────────

def eval_tensor_at(t, subsmap, depth_idx):
    """Exact-substitute every component; return list of nonzero (idx, val)."""
    nz = []
    for idx in depth_idx:
        expr = t
        for i in idx:
            expr = expr[i]
        v = sp.nsimplify(sp.together(expr).subs(subsmap))
        v = sp.cancel(v)
        if v != 0:
            nz.append((idx, v))
    return nz


def signature_by_minors(gm: sp.Matrix):
    """Signature via Jacobi/Sylvester: signs of leading principal minors.
    Valid when all leading minors are nonzero (checked)."""
    n = gm.shape[0]
    minors = [gm[:k, :k].det() for k in range(1, n + 1)]
    assert all(mv != 0 for mv in minors), "degenerate leading minor"
    signs = [sp.sign(mv) for mv in minors]
    neg = 0
    prev = 1
    for s in signs:
        if s * prev < 0:
            neg += 1
        prev = s
    return (n - neg, neg), minors


# ── main ──────────────────────────────────────────────────────────────

def main(argv=None) -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--m", type=int, default=4,
                    help="ambient outcomes (simplex dim = m−1); default 4")
    ap.add_argument("--fast", action="store_true",
                    help="defer simplification; point-evaluation only "
                         "(≠0 results remain exact proofs; ≡0 results are "
                         "sample-point evidence, confirm symbolically later)")
    args = ap.parse_args(argv)
    if args.fast:
        sp.together = lambda x: x  # simplification deferred to point-eval
    m = args.m
    n = m - 1

    ps = sp.symbols(f"p1:{m}", positive=True)
    coords = list(ps)
    g = fisher_metric(m, ps)

    # rossbyDriftFromChirality weights (BraidStateN.lean):
    #   left=+1, right=−1, scarred=+1/2, achiral=0 — cycled over strands.
    base = [sp.Integer(1), sp.Integer(-1), sp.Rational(1, 2), sp.Integer(0)]
    w = [base[i % 4] for i in range(m)]
    wbar = sp.Rational(sum(w), m)
    beta_amb = [wi - wbar for wi in w]          # Σ = 0: tangent
    beta = sp.Matrix(beta_amb[:n])              # reduced-chart components
    print(f"Δ_{n}: m={m} outcomes, chirality weights w={w}, β_reduced={list(beta)}")

    # ---- 0a: baseline ------------------------------------------------
    print("\n[0a] static Fisher–Rao baseline")
    Gamma = christoffel(g, coords)
    Rup, Rdn = riemann(g, Gamma, coords)
    K = sp.Rational(1, 4)
    csc_ok = True
    for i, j, k, l in itertools.product(range(n), repeat=4):
        expect = K * (g[i, k] * g[j, l] - g[i, l] * g[j, k])
        if sp.simplify(sp.together(Rdn[i][j][k][l] - expect)) != 0:
            csc_ok = False
            break
    print(f"  R = 1/4 (g∧g) (constant curvature 1/4, symbolic): {csc_ok}")
    if csc_ok:
        print("  ⇒ ∇R ≡ 0 (constant-curvature ⇒ locally symmetric): True")
        print("  ⇒ signature (m−1, 0) positive-definite, holonomy SO(m−1)")
    else:
        print("  UNEXPECTED — baseline is not constant curvature; abort")
        return 1

    # ---- 0b: drift-flipped metric -------------------------------------
    print("\n[0b] drift-flipped metric g' = g − 2 β♭⊗β♭ / g(β,β)")
    beta_flat = g * beta
    beta_norm2 = (beta.T * g * beta)[0, 0]
    gp = sp.Matrix(n, n, lambda i, j: sp.together(
        g[i, j] - 2 * beta_flat[i] * beta_flat[j] / beta_norm2))

    # signature at rational sample points (exact minors)
    pts = []
    centroid = {ps[i]: sp.Rational(1, m) for i in range(n)}
    pts.append(("centroid", centroid))
    off1 = {ps[i]: sp.Rational(i + 2, 2 * m * (m + 2)) for i in range(n)}
    pts.append(("offcenter1", off1))
    off2 = {ps[i]: sp.Rational(2 * i + 1, m * m + 3) for i in range(n)}
    pts.append(("offcenter2", off2))

    for name, pt in pts:
        gm = sp.Matrix(n, n, lambda i, j: sp.cancel(gp[i, j].subs(pt)))
        sig, _ = signature_by_minors(gm)
        # time-like = drift: g'(β,β) = −g(β,β) < 0
        bb = sp.cancel((beta.T * gm * beta)[0, 0])
        print(f"  {name}: signature {sig}, g'(β,β) = {bb} (<0 ⇒ β time-like)")

    # ---- 0c: discriminator on g' --------------------------------------
    print("\n[0c] discriminator on g'")
    Gp = christoffel(gp, coords)
    Rpu, Rpd = riemann(gp, Gp, coords)

    idx4 = list(itertools.product(range(n), repeat=4))
    idx5 = list(itertools.product(range(n), repeat=5))

    nablaR = cov_deriv_riemann(Rpd, Gp, coords)
    name0, pt0 = pts[0]
    nz = eval_tensor_at(nablaR, pt0, idx5)
    print(f"  ∇R' at {name0}: {len(nz)} nonzero components "
          f"{'⇒ NOT locally symmetric' if nz else '⇒ vanishes here'}")
    if nz:
        idx, v = nz[0]
        print(f"    e.g. (∇R')_{idx} = {v}")

    RR, Q = rr_and_q(gp, Rpu, Rpd, n)
    max_rr_nz = 0
    ratios = {}
    ok_pseudo = True
    L_val = None
    for (a, b) in RR:
        for name, pt in pts[:2]:
            rr_nz = {i: sp.cancel(sp.together(RRc).subs(pt))
                     for i, RRc in _iter4(RR[(a, b)], idx4)}
            q_nz = {i: sp.cancel(sp.together(Qc).subs(pt))
                    for i, Qc in _iter4(Q[(a, b)], idx4)}
            rr_nz = {i: v for i, v in rr_nz.items() if v != 0}
            q_nz = {i: v for i, v in q_nz.items() if v != 0}
            max_rr_nz = max(max_rr_nz, len(rr_nz))
            if not rr_nz:
                continue
            # pseudosymmetry: R·R = L · Q(g,R) componentwise
            for i, v in rr_nz.items():
                if i not in q_nz:
                    ok_pseudo = False
                    break
                r = sp.cancel(v / q_nz[i])
                ratios.setdefault((name, a, b), set()).add(r)
            if set(rr_nz) != set(q_nz):
                # Q has support where R·R doesn't (or vice versa) → L=0 forced there
                extra = set(q_nz) - set(rr_nz)
                if extra:
                    ratios.setdefault((name, a, b), set()).add(sp.S.Zero)

    if max_rr_nz == 0:
        print("  R'·R' = 0 at sample points (candidate SEMISYMMETRIC — "
              "confirm symbolically)")
        if args.fast:
            verdict = ("SEMISYMMETRIC (Szabó) at sample points — symbolic "
                       "≡ 0 confirmation pending (rerun without --fast)")
        else:
            allzero = all(sp.simplify(sp.together(c)) == 0
                          for (a, b) in RR for _, c in _iter4(RR[(a, b)], idx4))
            print(f"  R'·R' ≡ 0 symbolically: {allzero}")
            verdict = "SEMISYMMETRIC (Szabó)" if allzero and nz else "check"
    else:
        per_point = {}
        for (name, a, b), rs in ratios.items():
            per_point.setdefault(name, set()).update(rs)
        for name, rs in per_point.items():
            print(f"  L candidates at {name}: {rs}")
        consistent = all(len(rs) == 1 for rs in per_point.values()) and ok_pseudo
        if consistent:
            L_val = {name: next(iter(rs)) for name, rs in per_point.items()}
            verdict = f"PSEUDOSYMMETRIC (Deszcz), L = {L_val}"
        else:
            verdict = ("PROPER (neither semi- nor pseudo-symmetric at sample "
                       "points) — R·R ≠ L·Q(g,R)")
    print(f"\n[verdict] baseline: LOCALLY SYMMETRIC (∇R=0). "
          f"drift-perturbed: {'∇R ≠ 0; ' if nz else ''}{verdict}")

    # ---- 0d: drift-direction diagnostics ------------------------------
    print("\n[0d] obstruction vs drift direction (centroid)")
    ib = [sp.S.Zero] * 4
    # contract ∇R' with β in the derivative slot vs a g-orthogonal vector
    contr_beta = sp.S.Zero
    for a in range(n):
        for idx in idx4:
            comp = nablaR[a][idx[0]][idx[1]][idx[2]][idx[3]]
            contr_beta += (beta[a] * comp.subs(pt0)) ** 2
    contr_beta = sp.cancel(contr_beta)
    print(f"  Σ (β^a ∇_a R')² at centroid = {sp.nsimplify(contr_beta)} "
          f"({'drift direction carries obstruction' if contr_beta != 0 else 'obstruction ⟂ drift'})")
    return 0


def _iter4(t, idx4):
    for idx in idx4:
        yield idx, t[idx[0]][idx[1]][idx[2]][idx[3]]


if __name__ == "__main__":
    sys.exit(main())
