"""Hopf fibers, full transverse tensors, and horizontal transport.

Standalone LAB05 release. No repository imports, network, or random inputs.
Default: JSON on stdout. Optional --figure PATH writes the native SVG.
Both angular parameters have their declared orientations; the fiber has period
2*pi. The numerical ODE is not renormalized after steps, so drift stays visible.
"""
import argparse
import json
import math
from pathlib import Path

import mpmath as mp
import sympy as sp


def exact_checks():
    y = sp.Matrix(sp.symbols("y0:4", real=True))
    unit = sum(t*t for t in y) - 1
    J = sp.Matrix([[0, -1, 0, 0], [1, 0, 0, 0],
                   [0, 0, 0, -1], [0, 0, 1, 0]])
    v = J*y
    P = sp.eye(4) - y*y.T
    H = P - v*v.T
    Phi = -J*H
    count = 0

    def zero(value):
        nonlocal count
        values = list(value) if isinstance(value, sp.MatrixBase) else [value]
        for x in values:
            numerator = sp.cancel(x).as_numer_denom()[0]
            assert sp.rem(numerator, unit, *y) == 0, sp.factor(numerator)
        count += 1

    zero(J.T*J-sp.eye(4))
    zero(P*P-P)
    zero(H*H-H)
    zero(P*y)
    zero(H*v)
    zero(P*v-v)
    zero((v.T*v)[0]-1)
    zero(Phi*Phi+H)
    zero(Phi*y)
    zero(Phi*v)
    zero(v.T*Phi)
    zero(Phi.T*Phi-H)
    zero(Phi.T+Phi)
    # A full mixed tensor Lie derivative, not a componentwise derivative alone.
    derivative = Phi.applyfunc(lambda f: sum(sp.diff(f, y[i])*v[i]
                                            for i in range(4)))
    zero(derivative-J*Phi+Phi*J)
    n = sp.Matrix([2*(y[0]*y[2]+y[1]*y[3]),
                   2*(y[0]*y[3]-y[1]*y[2]),
                   y[0]**2+y[1]**2-y[2]**2-y[3]**2])
    dn = n.jacobian(y)
    zero((n.T*n)[0]-1)
    zero(dn*v)
    zero((dn*H).T*(dn*H)-4*H)
    # Curvature is dTheta=2(dy0^dy1+dy2^dy3). No sample estimates c1.
    curvature = -2*J
    zero(P*curvature*v)
    th, ph, ch = sp.symbols("theta phi chi", real=True)
    z = sp.Matrix([sp.exp(sp.I*ch)*sp.cos(th/2),
                   sp.exp(sp.I*(ch+ph))*sp.sin(th/2)])
    pullback = [sp.simplify(-sp.I*(sp.conjugate(z).T*z.diff(q))[0])
                for q in (ch, th, ph)]
    assert pullback == [1, 0, sp.sin(th/2)**2]
    count += 1
    flux = sp.integrate(sp.sin(th)/2, (th, 0, sp.pi))*2*sp.pi
    assert flux/(2*sp.pi) == 1
    count += 1
    # A deliberately leaked vertical component passes the projected test only.
    north = {y[0]: 1, y[1]: 0, y[2]: 0, y[3]: 0}
    h0, p0, v0 = H.subs(north), Phi.subs(north), v.subs(north)
    u = sp.Matrix([0, 0, 1, 0])
    bad = p0 + sp.Rational(1, 3)*v0*u.T
    assert h0*bad*h0 == p0
    assert (v0.T*bad*u)[0] == sp.Rational(1, 3)
    assert (bad*u).dot(bad*u) == sp.Rational(10, 9)
    count += 3
    return count


def pauli(w):
    return mp.matrix([[w[2], w[0]-1j*w[1]],
                      [w[0]+1j*w[1], -w[2]]])


def data(c):
    s = mp.sqrt(1-c*c)
    axis = mp.matrix([s, 0, -c])
    b = mp.matrix([c, 0, s])
    d = mp.matrix([0, -1, 0])
    z0 = mp.matrix([s, c])

    def base(t):
        return c*axis+s*(mp.cos(t)*b+mp.sin(t)*d)

    def velocity(t):
        return s*(-mp.sin(t)*b+mp.cos(t)*d)

    def exact(t):
        rotation = mp.cos(t/2)*mp.eye(2)-1j*mp.sin(t/2)*pauli(axis)
        return mp.exp(1j*c*t/2)*rotation*z0

    return base, velocity, exact, z0


def integrate(c, steps):
    base, velocity, exact, z0 = data(c)
    h = 2*mp.pi/steps
    z = z0.copy()
    error = norm_drift = line_defect = mp.mpf(0)

    def rhs(t, state):
        return pauli(velocity(t))*state/2

    for j in range(steps):
        t = j*h
        k1 = rhs(t, z)
        k2 = rhs(t+h/2, z+h*k1/2)
        k3 = rhs(t+h/2, z+h*k2/2)
        k4 = rhs(t+h, z+h*k3)
        z += h*(k1+2*k2+2*k3+k4)/6
        t = (j+1)*h
        error = max(error, mp.norm(z-exact(t)))
        norm_drift = max(norm_drift, abs((z.H*z)[0]-1))
        line_defect = max(line_defect, mp.norm((mp.eye(2)-pauli(base(t)))*z/2))
    phase = (z0.H*z)[0]
    return {"steps": steps, "max_vector_error": mp.nstr(error, 12),
            "max_norm_drift": mp.nstr(norm_drift, 12),
            "max_line_defect": mp.nstr(line_defect, 12),
            "endpoint_overlap_real": mp.nstr(mp.re(phase), 12),
            "endpoint_overlap_imag": mp.nstr(mp.im(phase), 12)}, error


def figure(path):
    """Original SVG from literal fibers, no image library. Projection ell=1."""
    # Viewing axes form an oriented orthonormal basis; depth sorting identifies
    # overpasses in the two-dimensional drawing, not intersections in R3.
    ex = (3/math.sqrt(13), -2/math.sqrt(13), 0)
    ey = (10/math.sqrt(494), 15/math.sqrt(494), -13/math.sqrt(494))
    ez = (2/math.sqrt(38), 3/math.sqrt(38), 5/math.sqrt(38))

    def dot(a, b):
        return sum(x*y for x, y in zip(a, b))

    def point(t, sign):
        q = 1/math.sqrt(2)
        y = (q*math.cos(t), q*math.sin(t),
             sign*q*math.cos(t), sign*q*math.sin(t))
        # The minus sign makes this chart preserve the outward S3 orientation.
        r = (-y[1]/(1-y[0]), y[2]/(1-y[0]), y[3]/(1-y[0]))
        return (300+85*dot(r, ex), 275-85*dot(r, ey), dot(r, ez))

    colors = ("#1760a5", "#b34800")
    nodes = [[point(2*math.pi*j/480, sign) for j in range(481)] for sign in (1,-1)]
    def svg_path(points):
        return 'M '+' L '.join(f'{p[0]:.3f},{p[1]:.3f}' for p in points)
    svg = ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 530" role="img" aria-labelledby="title desc">',
           '<title id="title">Two Hopf fibers in stereographic projection</title>',
           '<desc id="desc">Blue and orange fibers over opposite equatorial points form two linked circles. Both are oriented by increasing common phase chi. The projection omits y=(1,0,0,0), which belongs to neither displayed fiber. White gaps at crossings indicate depth in the drawing.</desc>',
           '<rect width="600" height="530" rx="12" fill="#fff"/>',
           '<g font-family="Arial,sans-serif" fill="#182331">',
           '<text x="24" y="33" font-size="21" font-weight="600">Two fibers, two different base points</text>',
           '<text x="24" y="61" font-size="15">Stereographic R³ view; radius ℓ = 1</text>',
           '<text x="24" y="91" fill="#1760a5" font-size="16">Blue: n = (1, 0, 0)</text>',
           '<text x="315" y="91" fill="#b34800" font-size="16">Orange: n = (−1, 0, 0)</text>', '</g>']
    for pts, color in zip(nodes,colors):
        svg.append(f'<path d="{svg_path(pts)}" fill="none" stroke="{color}" stroke-width="3.5"/>')
    # Find actual transverse planar crossings, then repaint only the short
    # front arc. This preserves continuous fibers and makes depth legible.
    for i in range(480):
        a,b = nodes[0][i:i+2]
        ux,uy = b[0]-a[0],b[1]-a[1]
        for j in range(480):
            c,d = nodes[1][j:j+2]
            vx,vy = d[0]-c[0],d[1]-c[1]
            determinant = ux*vy-uy*vx
            if abs(determinant)<1e-12:
                continue
            qx,qy = c[0]-a[0],c[1]-a[1]
            alpha=(qx*vy-qy*vx)/determinant
            beta=(qx*uy-qy*ux)/determinant
            if not (0<alpha<1 and 0<beta<1):
                continue
            depth0=a[2]+alpha*(b[2]-a[2])
            depth1=c[2]+beta*(d[2]-c[2])
            which,center=(0,i+alpha) if depth0>depth1 else (1,j+beta)
            pts=[point(2*math.pi*(center+k/4)/480,(1,-1)[which]) for k in range(-28,29)]
            path_data=svg_path(pts)
            svg.append(f'<path d="{path_data}" fill="none" stroke="#fff" stroke-width="9"/>')
            svg.append(f'<path d="{path_data}" fill="none" stroke="{colors[which]}" stroke-width="3.5"/>')
    # Arrowheads tangent to each parametrized fiber, away from crossings.
    for sign, color, t in ((1, colors[0], 3.1), (-1, colors[1], 1.15)):
        a, b = point(t-.02, sign), point(t, sign)
        dx, dy = b[0]-a[0], b[1]-a[1]
        size = math.hypot(dx, dy)
        dx, dy = dx/size, dy/size
        pts = [(b[0]+8*dx, b[1]+8*dy),
               (b[0]-7*dx+5*dy, b[1]-7*dy-5*dx),
               (b[0]-7*dx-5*dy, b[1]-7*dy+5*dx)]
        svg.append('<polygon points="'+' '.join(f'{x:.2f},{y:.2f}' for x,y in pts)+f'" fill="{color}"/>')
    svg += ['<g font-family="Arial,sans-serif" font-size="15" fill="#182331">',
            '<text x="24" y="469">Arrows: increasing χ, with period 2π.</text>',
            '<text x="24" y="494">Missing point: y = (1, 0, 0, 0); neither fiber reaches it.</text>',
            '<text x="24" y="518">The planar crossings are projection overlaps, not intersections.</text>',
            '</g></svg>']
    Path(path).write_text('\n'.join(svg)+'\n', encoding='utf-8')


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--cosine', default='1/2', help='latitude cosine, e.g. 1/3; strictly between -1 and 1')
    parser.add_argument('--dps', type=int, default=50, help='decimal working precision, at least 30')
    parser.add_argument('--figure', metavar='PATH', help='write the reproducible native SVG here')
    args = parser.parse_args()
    rational = sp.Rational(args.cosine)
    if not -1 < rational < 1 or args.dps < 30:
        parser.error('require -1 < cosine < 1 and dps >= 30')
    mp.mp.dps = args.dps
    c = mp.mpf(str(rational.p))/int(rational.q)
    rows, errors = [], []
    for steps in (64, 128, 256):
        row, err = integrate(c, steps)
        rows.append(row)
        errors.append(err)
    expected_phase = mp.exp(-1j*mp.pi*(1-c))
    result = {"geometry": "round S3, normalized U1 connection, no supergravity sources selected",
              "fiber_period": "2*pi", "chern_number_exact": 1,
              "exact_grouped_assertions": exact_checks(),
              "latitude_cosine": str(rational), "decimal_precision": args.dps,
              "expected_holonomy_real": mp.nstr(mp.re(expected_phase), 12),
              "expected_holonomy_imag": mp.nstr(mp.im(expected_phase), 12),
              "north_section_missing_point_crossed": True,
              "rk4": rows,
              "successive_error_ratios": [mp.nstr(errors[i]/errors[i+1], 12) for i in range(2)],
              "vertical_leak_control": {"projected_test_passes": True,
                                         "eta_Phi_u": "1/3", "squared_norm_Phi_u": "10/9"}}
    if args.figure:
        figure(args.figure)
    print(json.dumps(result, indent=2))


if __name__ == '__main__':
    main()
