"""Neutrino Oscillation Closure — SM.INTSTACK.v1

Computes neutrino flavor oscillation probabilities P(ν_α → ν_β; L, E)
and maps the oscillation phenomenon to GCD kernel invariants.

Physics:
  In vacuum, the probability of a neutrino produced in flavor α being
  detected in flavor β after propagating baseline L with energy E is:

    P(ν_α → ν_β) = δ_αβ
      − 4 Σ_{i>j} Re(U_αi U*_βi U*_αj U_βj) sin²(Δm²_ij L / 4E)
      + 2 Σ_{i>j} Im(U_αi U*_βi U*_αj U_βj) sin(Δm²_ij L / 2E)

  For antineutrinos, the sign of the imaginary (CP-violating) term flips.

  DUNE/LBNF specifics:
    Baseline:  L = 1285 km  (Fermilab → SURF, Lead, SD)
    Peak energy: E ≈ 2.5 GeV  (wide-band beam, covers 0.5–5 GeV)
    Channel:   ν_μ → ν_e appearance  (primary oscillation signal)
    Goals:     (1) δ_CP measurement to ≤ 10°
               (2) Mass ordering determination (>5σ)
               (3) θ₂₃ octant resolution

  GCD mapping — Neutrino oscillation as channel drift:
    At production (L=0):  flavor trace = [1, 0, 0]  (pure ν_μ)
    At detection (L>0):   flavor trace = [P_μe, P_μμ, P_μτ]
    The trace vector drifts from a maximally heterogeneous state
    (one channel at 1, two at ε) toward democratic mixing.
    Oscillation IS derivatio — a periodic drift with measured return.

  Key structural insight:
    The oscillation period defines τ_R: the system returns to its
    initial state at L/E = 4πE/Δm² (first full oscillation).
    This is a literal, measurable return — *solum quod redit, reale est*.

UMCP integration:
  Trace vector: c = [P_αe, P_αμ, P_ατ]  (flavor probabilities)
  Weights: w = [1/3, 1/3, 1/3]  (equal — no flavor is privileged)
  Guard band: ε = 10⁻⁶

  F(L/E) = (1/3) Σ_β P(ν_α → ν_β)  → must = 1/3 by probability conservation
  IC(L/E) = [Π_β P(ν_α → ν_β)]^{1/3} → varies with L/E
  Δ(L/E) = F − IC  → heterogeneity gap: maximal at production, min at democratic mixing
  ω_eff computed from oscillation departure from vacuum expectation

Regime classification:
  Vacuum:     No matter effects (ρ → 0)
  Resonant:   Near MSW resonance (matter-enhanced mixing)
  Suppressed: High density suppresses oscillation

Cross-references:
  Contract:  contracts/SM.INTSTACK.v1.yaml
  Sources:   NuFIT 5.3 (2024), DUNE CDR (2020), Pontecorvo (1957), MNS (1962),
             Wolfenstein (1978), Mikheyev-Smirnov (1985)
  Sibling:   closures/standard_model/pmns_mixing.py (PMNS matrix)
  Sibling:   closures/standard_model/ckm_mixing.py (quark mixing analog)
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from enum import StrEnum
from typing import Any

import numpy as np

from closures.standard_model.pmns_mixing import (
    DELTA_CP_DEG,
    DM2_21,
    DM2_32,
    SIN2_THETA12,
    SIN2_THETA13,
    SIN2_THETA23,
    compute_pmns_mixing,
)
from src.umcp.kernel_optimized import compute_kernel_outputs

# ── Constants ────────────────────────────────────────────────────

# Guard band (frozen, consistent across the seam)
EPSILON = 1e-6

# Natural unit conversion: Δm² [eV²] × L [km] / E [GeV]
# Need factor 1.267 for sin²(1.267 Δm² L / E)
# Exact: ℏc / (4 × 10⁹ eV⋅fm × 10¹⁸ fm/km) = 1.26693...
OSCILLATION_FACTOR = 1.26693  # dimensionless prefactor

# DUNE/LBNF parameters
DUNE_BASELINE_KM = 1285.0  # Fermilab → SURF (km)
DUNE_PEAK_ENERGY_GEV = 2.5  # Wide-band beam peak (GeV)
DUNE_ENERGY_RANGE_GEV = (0.5, 8.0)  # Usable energy range

# Earth matter density along DUNE baseline (approximate)
EARTH_DENSITY_G_CM3 = 2.848  # average mantle density (g/cm³)
ELECTRON_FRACTION = 0.5  # Y_e for Earth mantle (protons/nucleons)

# Fermi constant for matter potential
G_FERMI = 1.1663788e-5  # GeV⁻²
N_AVOGADRO = 6.02214076e23  # mol⁻¹
# V_CC = √2 G_F N_e  where N_e = ρ Y_e N_A / (1 g/mol)
# In eV: V_CC ≈ 7.63 × 10⁻¹⁴ eV × (ρ / g⋅cm⁻³) × Y_e
MATTER_POTENTIAL_PREFACTOR = 7.63e-14  # eV per (g/cm³ × Y_e)


class OscillationRegime(StrEnum):
    VACUUM = "Vacuum"
    RESONANT = "Resonant"
    SUPPRESSED = "Suppressed"


class MassOrdering(StrEnum):
    NORMAL = "Normal"  # m₁ < m₂ < m₃
    INVERTED = "Inverted"  # m₃ < m₁ < m₂


@dataclass
class OscillationPoint:
    """Oscillation probability at a single (L, E) point."""

    L_km: float
    E_GeV: float
    L_over_E: float
    P_ee: float
    P_emu: float
    P_etau: float
    P_mue: float
    P_mumu: float
    P_mutau: float
    P_taue: float
    P_taumu: float
    P_tautau: float
    unitarity_e: float  # P_ee + P_emu + P_etau (should = 1)
    unitarity_mu: float
    unitarity_tau: float
    # Kernel invariants for ν_μ row (DUNE primary channel)
    F_mu: float
    IC_mu: float
    kappa_mu: float
    heterogeneity_gap_mu: float
    regime: str


@dataclass
class OscillationSweep:
    """Result of sweeping L/E to map oscillation → kernel flow."""

    channel: str  # e.g. "nu_mu"
    n_points: int
    L_over_E_values: list[float]
    F_values: list[float]
    IC_values: list[float]
    gap_values: list[float]  # Δ = F − IC
    kappa_values: list[float]
    entropy_values: list[float]
    # Oscillation return time
    tau_R_first: float  # L/E at first return (P_αα returns to ~1)
    tau_R_period: float  # Oscillation period in L/E
    # Extrema
    F_min: float
    F_max: float
    IC_min: float
    IC_max: float
    gap_max: float  # Maximum heterogeneity gap
    gap_min: float
    # Phase diagram classification
    n_stable: int
    n_watch: int
    n_collapse: int


@dataclass
class DUNEPrediction:
    """GCD kernel predictions for DUNE/LBNF."""

    baseline_km: float
    peak_energy_GeV: float
    # Oscillation probabilities at DUNE operating point
    P_mue_vacuum: float  # ν_μ → ν_e appearance (vacuum)
    P_mue_matter: float  # ν_μ → ν_e appearance (matter)
    P_mumu_vacuum: float  # ν_μ → ν_μ survival (vacuum)
    P_mumu_matter: float  # ν_μ → ν_μ survival (matter)
    # CP asymmetry
    A_CP_vacuum: float  # [P(ν) − P(ν̄)] / [P(ν) + P(ν̄)]
    A_CP_matter: float
    # Matter effect magnitude
    matter_enhancement: float  # P_matter / P_vacuum
    # Kernel invariants at DUNE operating point
    F_production: float  # F at L=0 (pure ν_μ)
    IC_production: float
    F_detection: float  # F at DUNE baseline
    IC_detection: float
    gap_production: float
    gap_detection: float
    # Mass ordering sensitivity
    delta_P_ordering: float  # |P(NO) − P(IO)| at DUNE
    ordering_verdict: str


@dataclass
class TheoremResult:
    """Result of testing one theorem — mirrors particle_physics_formalism."""

    name: str
    statement: str
    n_tests: int
    n_passed: int
    n_failed: int
    details: dict[str, Any]
    verdict: str  # "PROVEN" or "FALSIFIED"

    @property
    def pass_rate(self) -> float:
        return self.n_passed / self.n_tests if self.n_tests > 0 else 0.0


# ═══════════════════════════════════════════════════════════════════
# PMNS MATRIX CONSTRUCTION (complex, for oscillation amplitudes)
# ═══════════════════════════════════════════════════════════════════


def _build_pmns_complex(
    sin2_theta12: float = SIN2_THETA12,
    sin2_theta23: float = SIN2_THETA23,
    sin2_theta13: float = SIN2_THETA13,
    delta_CP_deg: float = DELTA_CP_DEG,
) -> np.ndarray:
    """Build the full complex 3×3 PMNS matrix.

    The oscillation formula requires the complex phases,
    not just the magnitudes from pmns_mixing.py.

    Returns
    -------
    U : np.ndarray, shape (3,3), dtype=complex128
    """
    s12 = math.sqrt(sin2_theta12)
    c12 = math.sqrt(1 - sin2_theta12)
    s23 = math.sqrt(sin2_theta23)
    c23 = math.sqrt(1 - sin2_theta23)
    s13 = math.sqrt(sin2_theta13)
    c13 = math.sqrt(1 - sin2_theta13)
    delta = math.radians(delta_CP_deg)
    e_id = complex(math.cos(delta), math.sin(delta))  # e^{iδ}
    e_mid = complex(math.cos(delta), -math.sin(delta))  # e^{-iδ}

    U = np.array(
        [
            [c12 * c13, s12 * c13, s13 * e_mid],
            [
                -s12 * c23 - c12 * s23 * s13 * e_id,
                c12 * c23 - s12 * s23 * s13 * e_id,
                s23 * c13,
            ],
            [
                s12 * s23 - c12 * c23 * s13 * e_id,
                -c12 * s23 - s12 * c23 * s13 * e_id,
                c23 * c13,
            ],
        ],
        dtype=complex,
    )
    return U


# ═══════════════════════════════════════════════════════════════════
# VACUUM OSCILLATION PROBABILITY
# ═══════════════════════════════════════════════════════════════════


def oscillation_probability_vacuum(
    alpha: int,
    beta: int,
    L_km: float,
    E_GeV: float,
    dm2_21: float = DM2_21,
    dm2_32: float = DM2_32,
    delta_CP_deg: float = DELTA_CP_DEG,
    antineutrino: bool = False,
) -> float:
    """Compute P(ν_α → ν_β) in vacuum.

    Uses the exact three-flavor oscillation formula.

    Parameters
    ----------
    alpha, beta : int
        Flavor indices (0=e, 1=μ, 2=τ)
    L_km : float
        Baseline in km
    E_GeV : float
        Neutrino energy in GeV
    dm2_21, dm2_32 : float
        Mass-squared differences in eV²
    delta_CP_deg : float
        CP phase in degrees
    antineutrino : bool
        If True, flip sign of CP-violating term

    Returns
    -------
    P : float in [0, 1]
    """
    # Mass-squared differences
    dm2_31 = dm2_32 + dm2_21
    dm2 = [dm2_21, dm2_31, dm2_32]  # (21, 31, 32)

    # Build complex PMNS matrix
    U = _build_pmns_complex(delta_CP_deg=delta_CP_deg)
    if antineutrino:
        U = np.conj(U)

    # Phase arguments: Δ_ij = Δm²_ij × L / (4E) in natural units
    # sin²(1.267 Δm² L / E) where Δm² in eV², L in km, E in GeV
    phases = [OSCILLATION_FACTOR * d * L_km / E_GeV for d in dm2]

    # Kronecker delta
    P = 1.0 if alpha == beta else 0.0

    # Sum over (i,j) pairs: (1,2)→dm2_21, (1,3)→dm2_31, (2,3)→dm2_32
    pairs = [(0, 1, 0), (0, 2, 1), (1, 2, 2)]  # (i, j, dm2_index)

    for i, j, di in pairs:
        # Product of PMNS elements: U_αi U*_βi U*_αj U_βj
        product = U[alpha, i] * np.conj(U[beta, i]) * np.conj(U[alpha, j]) * U[beta, j]
        re_part = float(product.real)
        im_part = float(product.imag)
        sin2_phase = math.sin(phases[di]) ** 2
        sin_2phase = math.sin(2 * phases[di])

        P -= 4 * re_part * sin2_phase
        P += 2 * im_part * sin_2phase

    # Clamp to [0, 1] (numerical precision)
    return max(0.0, min(1.0, P))


def oscillation_matrix_vacuum(
    L_km: float,
    E_GeV: float,
    dm2_21: float = DM2_21,
    dm2_32: float = DM2_32,
    delta_CP_deg: float = DELTA_CP_DEG,
    antineutrino: bool = False,
) -> np.ndarray:
    """Compute full 3×3 oscillation probability matrix P_αβ.

    Returns
    -------
    P : np.ndarray, shape (3,3)
        P[α, β] = P(ν_α → ν_β)
    """
    P = np.zeros((3, 3))
    for a in range(3):
        for b in range(3):
            P[a, b] = oscillation_probability_vacuum(
                a,
                b,
                L_km,
                E_GeV,
                dm2_21=dm2_21,
                dm2_32=dm2_32,
                delta_CP_deg=delta_CP_deg,
                antineutrino=antineutrino,
            )
    return P


# ═══════════════════════════════════════════════════════════════════
# MATTER EFFECTS (MSW — Mikheyev-Smirnov-Wolfenstein)
# ═══════════════════════════════════════════════════════════════════


def _matter_potential(rho_g_cm3: float, Y_e: float = ELECTRON_FRACTION) -> float:
    """Compute the charged-current matter potential V_CC in eV.

    V_CC = √2 G_F N_e
         ≈ 7.63 × 10⁻¹⁴ eV × (ρ / g⋅cm⁻³) × Y_e

    Parameters
    ----------
    rho_g_cm3 : float
        Matter density in g/cm³
    Y_e : float
        Electron fraction (protons per nucleon)

    Returns
    -------
    V_CC : float in eV
    """
    return MATTER_POTENTIAL_PREFACTOR * rho_g_cm3 * Y_e


def oscillation_probability_matter(
    alpha: int,
    beta: int,
    L_km: float,
    E_GeV: float,
    rho_g_cm3: float = EARTH_DENSITY_G_CM3,
    dm2_21: float = DM2_21,
    dm2_32: float = DM2_32,
    delta_CP_deg: float = DELTA_CP_DEG,
    antineutrino: bool = False,
) -> float:
    """Compute P(ν_α → ν_β) with constant-density matter effects.

    Uses the effective two-flavor approximation for the dominant
    atmospheric Δm² channel, with the solar term treated perturbatively.
    This is adequate for DUNE energies (E ~ 1-5 GeV) and baselines.

    For ν_μ → ν_e (DUNE primary channel), the probability in matter is
    approximately (Cervera et al., 2000):

      P_μe ≈ sin²(2θ₁₃_m) sin²(θ₂₃) sin²(Δ₃₁_m)
           + α sin(2θ₁₃_m) sin(2θ₁₂) sin(2θ₂₃) sin(Δ₃₁_m) cos(Δ₃₁_m ± δ_CP)
           + α² cos²(θ₂₃) sin²(2θ₁₂) sin²(Δ₃₁_m)

    where α = Δm²₂₁/Δm²₃₁ and matter-modified θ₁₃_m and Δ₃₁_m.

    Parameters
    ----------
    alpha, beta : int
        Flavor indices (0=e, 1=μ, 2=τ)
    L_km : float
        Baseline in km
    E_GeV : float
        Neutrino energy in GeV
    rho_g_cm3 : float
        Matter density in g/cm³
    antineutrino : bool
        If True, flip sign of matter potential and CP term

    Returns
    -------
    P : float in [0, 1]
    """
    # For channels other than μ→e, fall back to vacuum
    # (matter effects are subdominant for disappearance channels)
    if not (alpha == 1 and beta == 0):
        return oscillation_probability_vacuum(alpha, beta, L_km, E_GeV, dm2_21, dm2_32, delta_CP_deg, antineutrino)

    # ν_μ → ν_e in matter (Cervera et al. approximation)
    dm2_31 = dm2_32 + dm2_21
    alpha_ratio = dm2_21 / dm2_31  # small parameter α ≈ 0.03

    s12 = math.sqrt(SIN2_THETA12)
    s23 = math.sqrt(SIN2_THETA23)
    s13 = math.sqrt(SIN2_THETA13)
    sin2_2theta13 = 4 * SIN2_THETA13 * (1 - SIN2_THETA13)
    delta = math.radians(delta_CP_deg)

    # Matter potential
    V_CC = _matter_potential(rho_g_cm3)
    # A = 2 E V_CC / Δm²₃₁  (dimensionless matter parameter)
    # Convert E from GeV to eV, Δm² is in eV²
    A = 2 * E_GeV * 1e9 * V_CC / abs(dm2_31)
    if antineutrino:
        A = -A

    # Matter-modified mixing
    sin2_2theta13_m = sin2_2theta13 / ((1 - A) ** 2 + sin2_2theta13 * A**2 / (1 - A) ** 2)
    # More stable form:
    denom = (math.cos(2 * math.asin(s13)) - A) ** 2 + sin2_2theta13
    sin2_2theta13_m = sin2_2theta13 / denom if denom > 0 else sin2_2theta13

    # Matter-modified phase
    # Δ₃₁_m = Δm²₃₁_m L / (4E)
    # Δm²₃₁_m = Δm²₃₁ √[(cos2θ₁₃ − A)² + sin²2θ₁₃]
    cos2theta13 = 1 - 2 * SIN2_THETA13
    dm2_31_m = abs(dm2_31) * math.sqrt((cos2theta13 - A) ** 2 + sin2_2theta13)
    Delta_m = OSCILLATION_FACTOR * dm2_31_m * L_km / E_GeV

    # Leading term
    P_lead = sin2_2theta13_m * SIN2_THETA23 * math.sin(Delta_m) ** 2

    # Interference term (CP-violating)
    cp_sign = -1 if antineutrino else 1
    P_inter = (
        alpha_ratio
        * math.sqrt(sin2_2theta13_m)
        * math.sin(2 * math.asin(s12))
        * math.sin(2 * math.asin(s23))
        * math.sin(Delta_m)
        * math.cos(Delta_m + cp_sign * delta)
    )

    # Solar term
    P_solar = alpha_ratio**2 * (1 - SIN2_THETA23) * (4 * SIN2_THETA12 * (1 - SIN2_THETA12)) * math.sin(Delta_m) ** 2

    P = P_lead + P_inter + P_solar
    return max(0.0, min(1.0, P))


# ═══════════════════════════════════════════════════════════════════
# KERNEL MAPPING — Oscillation probabilities → GCD trace vectors
# ═══════════════════════════════════════════════════════════════════


def _oscillation_to_kernel(P_row: list[float]) -> dict[str, Any]:
    """Map a row of oscillation probabilities to kernel invariants.

    The oscillation probability row [P_αe, P_αμ, P_ατ] is the trace
    vector: each element is a collapse channel representing how much
    of the initial flavor survives (diagonal) or converts (off-diagonal).

    Parameters
    ----------
    P_row : list of 3 floats
        [P(ν_α→ν_e), P(ν_α→ν_μ), P(ν_α→ν_τ)]

    Returns
    -------
    dict with F, omega, IC, kappa, S, C, heterogeneity_gap, regime
    """
    c = np.array(P_row, dtype=float)
    c = np.clip(c, EPSILON, 1 - EPSILON)
    w = np.ones(3) / 3.0  # Equal weights — no flavor privileged
    return compute_kernel_outputs(c, w, EPSILON)


def compute_oscillation_point(
    L_km: float,
    E_GeV: float,
    include_matter: bool = False,
    rho_g_cm3: float = EARTH_DENSITY_G_CM3,
) -> OscillationPoint:
    """Compute full oscillation probabilities and kernel invariants at (L, E).

    Parameters
    ----------
    L_km : float
        Baseline in km
    E_GeV : float
        Neutrino energy in GeV
    include_matter : bool
        Include MSW matter effects
    rho_g_cm3 : float
        Matter density (only used if include_matter=True)

    Returns
    -------
    OscillationPoint with all probabilities and kernel invariants
    """
    # Compute 3×3 probability matrix
    osc_func = oscillation_probability_matter if include_matter else oscillation_probability_vacuum
    kwargs: dict[str, Any] = {}
    if include_matter:
        kwargs["rho_g_cm3"] = rho_g_cm3

    P = np.zeros((3, 3))
    for a in range(3):
        for b in range(3):
            P[a, b] = osc_func(a, b, L_km, E_GeV, **kwargs)

    # Row unitarities
    u_e = float(P[0].sum())
    u_mu = float(P[1].sum())
    u_tau = float(P[2].sum())

    # Kernel invariants for ν_μ row (DUNE primary)
    mu_row = [float(P[1, 0]), float(P[1, 1]), float(P[1, 2])]
    k = _oscillation_to_kernel(mu_row)

    # Regime classification based on oscillation amplitude
    F_val = k["F"]
    IC_val = k["IC"]
    gap = F_val - IC_val

    return OscillationPoint(
        L_km=L_km,
        E_GeV=E_GeV,
        L_over_E=L_km / E_GeV if E_GeV > 0 else float("inf"),
        P_ee=float(P[0, 0]),
        P_emu=float(P[0, 1]),
        P_etau=float(P[0, 2]),
        P_mue=float(P[1, 0]),
        P_mumu=float(P[1, 1]),
        P_mutau=float(P[1, 2]),
        P_taue=float(P[2, 0]),
        P_taumu=float(P[2, 1]),
        P_tautau=float(P[2, 2]),
        unitarity_e=round(u_e, 8),
        unitarity_mu=round(u_mu, 8),
        unitarity_tau=round(u_tau, 8),
        F_mu=round(F_val, 6),
        IC_mu=round(IC_val, 6),
        kappa_mu=round(k["kappa"], 6),
        heterogeneity_gap_mu=round(gap, 6),
        regime=k["regime"],
    )


def compute_oscillation_sweep(
    L_over_E_range: tuple[float, float] = (0.0, 2000.0),
    n_points: int = 500,
    channel: str = "nu_mu",
    E_fixed_GeV: float = 2.5,
) -> OscillationSweep:
    """Sweep L/E and compute kernel invariants at each point.

    Maps how F, IC, Δ=F−IC, κ evolve with propagation — the kernel
    sees oscillation as a periodic drift-return cycle.

    Parameters
    ----------
    L_over_E_range : tuple
        (min, max) of L/E in km/GeV
    n_points : int
        Number of evaluation points
    channel : str
        "nu_e", "nu_mu", or "nu_tau"
    E_fixed_GeV : float
        Fixed energy for L variation

    Returns
    -------
    OscillationSweep with kernel trajectories
    """
    alpha_map = {"nu_e": 0, "nu_mu": 1, "nu_tau": 2}
    alpha = alpha_map.get(channel, 1)

    LE_vals = np.linspace(L_over_E_range[0] + 0.1, L_over_E_range[1], n_points)

    F_list: list[float] = []
    IC_list: list[float] = []
    gap_list: list[float] = []
    kappa_list: list[float] = []
    entropy_list: list[float] = []
    n_stable = 0
    n_watch = 0
    n_collapse = 0

    for LE in LE_vals:
        L = LE * E_fixed_GeV
        # Compute oscillation row for this flavor
        P_row = [oscillation_probability_vacuum(alpha, b, L, E_fixed_GeV) for b in range(3)]
        k = _oscillation_to_kernel(P_row)

        F_list.append(round(k["F"], 8))
        IC_list.append(round(k["IC"], 8))
        gap_list.append(round(k["F"] - k["IC"], 8))
        kappa_list.append(round(k["kappa"], 8))
        entropy_list.append(round(k["S"], 8))

        regime = k["regime"]
        if regime == "Stable":
            n_stable += 1
        elif regime == "Watch":
            n_watch += 1
        else:
            n_collapse += 1

    # Find first return: where P_αα comes back to within 5% of 1.0
    F_arr = np.array(F_list)
    IC_arr = np.array(IC_list)
    gap_arr = np.array(gap_list)

    # First oscillation minimum of survival probability
    # = first maximum of heterogeneity gap
    # The "return" is when the gap returns to near its initial (production) value
    tau_R_first = float("inf")
    tau_R_period = float("inf")

    # Find first local maximum of gap (first oscillation node)
    for i in range(1, len(gap_list) - 1):
        if gap_list[i] > gap_list[i - 1] and gap_list[i] > gap_list[i + 1]:
            # This is a local maximum — the oscillation "collapse" point
            # The "return" is the next local minimum after this
            for j in range(i + 1, len(gap_list) - 1):
                if gap_list[j] < gap_list[j - 1] and gap_list[j] < gap_list[j + 1]:
                    tau_R_first = float(LE_vals[j])
                    break
            break

    # Estimate oscillation period from atmospheric Δm²
    dm2_atm = abs(DM2_32)
    if dm2_atm > 0:
        # Full oscillation period: sin²(1.267 Δm² L/E) completes at L/E = π/1.267/Δm²
        tau_R_period = math.pi / (OSCILLATION_FACTOR * dm2_atm)

    return OscillationSweep(
        channel=channel,
        n_points=n_points,
        L_over_E_values=[round(float(x), 4) for x in LE_vals],
        F_values=F_list,
        IC_values=IC_list,
        gap_values=gap_list,
        kappa_values=kappa_list,
        entropy_values=entropy_list,
        tau_R_first=round(tau_R_first, 4),
        tau_R_period=round(tau_R_period, 4),
        F_min=round(float(F_arr.min()), 6),
        F_max=round(float(F_arr.max()), 6),
        IC_min=round(float(IC_arr.min()), 6),
        IC_max=round(float(IC_arr.max()), 6),
        gap_max=round(float(gap_arr.max()), 6),
        gap_min=round(float(gap_arr.min()), 6),
        n_stable=n_stable,
        n_watch=n_watch,
        n_collapse=n_collapse,
    )


# ═══════════════════════════════════════════════════════════════════
# DUNE PREDICTIONS
# ═══════════════════════════════════════════════════════════════════


def compute_dune_prediction(
    delta_CP_deg: float = DELTA_CP_DEG,
) -> DUNEPrediction:
    """Compute GCD kernel predictions for DUNE/LBNF.

    At DUNE's baseline (1285 km) and peak energy (2.5 GeV),
    compute oscillation probabilities in vacuum and matter,
    CP asymmetry, matter enhancement, and kernel invariants.

    Parameters
    ----------
    delta_CP_deg : float
        CP phase in degrees (swept in DUNE analysis)

    Returns
    -------
    DUNEPrediction with full GCD analysis
    """
    L = DUNE_BASELINE_KM
    E = DUNE_PEAK_ENERGY_GEV

    # Vacuum probabilities
    P_mue_vac = oscillation_probability_vacuum(1, 0, L, E, delta_CP_deg=delta_CP_deg)
    P_mumu_vac = oscillation_probability_vacuum(1, 1, L, E, delta_CP_deg=delta_CP_deg)
    P_mue_vac_bar = oscillation_probability_vacuum(1, 0, L, E, delta_CP_deg=delta_CP_deg, antineutrino=True)

    # Matter probabilities
    P_mue_mat = oscillation_probability_matter(1, 0, L, E, delta_CP_deg=delta_CP_deg)
    P_mumu_mat = oscillation_probability_matter(1, 1, L, E, delta_CP_deg=delta_CP_deg)
    P_mue_mat_bar = oscillation_probability_matter(1, 0, L, E, delta_CP_deg=delta_CP_deg, antineutrino=True)

    # CP asymmetry: A_CP = [P(ν) − P(ν̄)] / [P(ν) + P(ν̄)]
    A_CP_vac = (P_mue_vac - P_mue_vac_bar) / (P_mue_vac + P_mue_vac_bar) if (P_mue_vac + P_mue_vac_bar) > 1e-10 else 0.0
    A_CP_mat = (P_mue_mat - P_mue_mat_bar) / (P_mue_mat + P_mue_mat_bar) if (P_mue_mat + P_mue_mat_bar) > 1e-10 else 0.0

    # Matter enhancement
    matter_enh = P_mue_mat / P_mue_vac if P_mue_vac > 1e-10 else 1.0

    # Kernel at production (L=0 → pure ν_μ: [0, 1, 0])
    k_prod = _oscillation_to_kernel([EPSILON, 1 - EPSILON, EPSILON])

    # Kernel at detection (DUNE baseline)
    P_mu_row_det = [oscillation_probability_vacuum(1, b, L, E, delta_CP_deg=delta_CP_deg) for b in range(3)]
    k_det = _oscillation_to_kernel(P_mu_row_det)

    # Mass ordering sensitivity
    # Compute P_mue for inverted ordering (flip sign of Δm²₃₂)
    P_mue_IO = oscillation_probability_vacuum(1, 0, L, E, dm2_32=-abs(DM2_32), delta_CP_deg=delta_CP_deg)
    delta_P_ord = abs(P_mue_vac - P_mue_IO)
    ordering_verdict = "Resolvable" if delta_P_ord > 0.01 else "Marginal"

    return DUNEPrediction(
        baseline_km=L,
        peak_energy_GeV=E,
        P_mue_vacuum=round(P_mue_vac, 6),
        P_mue_matter=round(P_mue_mat, 6),
        P_mumu_vacuum=round(P_mumu_vac, 6),
        P_mumu_matter=round(P_mumu_mat, 6),
        A_CP_vacuum=round(A_CP_vac, 6),
        A_CP_matter=round(A_CP_mat, 6),
        matter_enhancement=round(matter_enh, 4),
        F_production=round(k_prod["F"], 6),
        IC_production=round(k_prod["IC"], 6),
        F_detection=round(k_det["F"], 6),
        IC_detection=round(k_det["IC"], 6),
        gap_production=round(k_prod["F"] - k_prod["IC"], 6),
        gap_detection=round(k_det["F"] - k_det["IC"], 6),
        delta_P_ordering=round(delta_P_ord, 6),
        ordering_verdict=ordering_verdict,
    )


# ═══════════════════════════════════════════════════════════════════
# THEOREM T11: NEUTRINO OSCILLATION AS PERIODIC CHANNEL DRIFT
# ═══════════════════════════════════════════════════════════════════


def theorem_T11_neutrino_oscillation_drift() -> TheoremResult:
    """T11: Neutrino Oscillation as Periodic Channel Drift.

    STATEMENT:
      Neutrino flavor oscillation P(ν_α → ν_β; L/E) maps to a
      periodic trajectory in GCD kernel space where:
        (a) F = 1/3 everywhere (unitarity forces constant fidelity)
        (b) IC oscillates between ε^{2/3} (production) and a maximum
            at democratic mixing
        (c) The heterogeneity gap Δ = F − IC is maximal at production
            and minimal at democratic mixing nodes
        (d) The oscillation period equals the kernel return time τ_R
        (e) Tier-1 identities (F + ω = 1, IC ≤ F) hold at every L/E
        (f) CP violation appears as an asymmetry in the kernel trajectory
            between neutrinos and antineutrinos

    PROOF SKETCH:
      At L=0, the flavor state is [1, 0, 0] → after clamping to [1−ε, ε, ε],
      F = (1−ε + 2ε)/3 ≈ 1/3 and IC = [(1−ε)·ε²]^{1/3} ≈ ε^{2/3}.
      As L/E increases, probability flows from the initial flavor to
      others via the PMNS matrix elements. At each point, the 3 probabilities
      sum to ~1 (unitarity), so F ≈ 1/3 always.
      IC peaks when probabilities equalize (P_αβ ≈ 1/3 for all β).
      The atmospheric oscillation period is L/E = π/(1.267·Δm²₃₂),
      which matches the kernel's τ_R (return time for the trace vector).

    WHY THIS MATTERS:
      This theorem demonstrates that oscillation — a quantum interference
      effect between mass eigenstates — is visible in the kernel as periodic
      channel drift (*derivatio periodica*). The neutrino literally drifts
      from one flavor channel to another and returns. This is Axiom-0 in
      action: collapse is generative (flavor changes are productive, creating
      new interaction channels), and only what returns is real (the oscillation
      period defines the return time).

      DUNE/LBNF will measure this with unprecedented precision. The 1285 km
      baseline at 2.5 GeV sits near the first oscillation maximum for ν_μ → ν_e,
      where the kernel's heterogeneity gap reaches its first extremum.
    """
    total_tests = 8
    passed = 0
    details: dict[str, Any] = {}

    # ── Test 1: F ≈ 1/3 everywhere along oscillation ──
    sweep = compute_oscillation_sweep(L_over_E_range=(1.0, 2000.0), n_points=200, channel="nu_mu")
    F_arr = np.array(sweep.F_values)
    F_mean = float(F_arr.mean())
    F_std = float(F_arr.std())
    # F should be very close to 1/3 = 0.3333... at all L/E
    t1_pass = abs(F_mean - 1 / 3) < 0.01 and F_std < 0.01
    details["F_mean"] = round(F_mean, 6)
    details["F_std"] = round(F_std, 8)
    details["F_target"] = round(1 / 3, 6)
    if t1_pass:
        passed += 1

    # ── Test 2: IC oscillates (not constant) ──
    IC_arr = np.array(sweep.IC_values)
    IC_range = float(IC_arr.max() - IC_arr.min())
    t2_pass = IC_range > 0.01  # IC must vary significantly
    details["IC_range"] = round(IC_range, 6)
    details["IC_min"] = round(float(IC_arr.min()), 6)
    details["IC_max"] = round(float(IC_arr.max()), 6)
    if t2_pass:
        passed += 1

    # ── Test 3: Heterogeneity gap maximal at production ──
    gap_arr = np.array(sweep.gap_values)
    # At L→0, the gap should be near-maximal (near 1/3 − ε^{2/3})
    gap_production = gap_arr[0]
    gap_max = float(gap_arr.max())
    # Gap at production should be > 90% of maximum gap observed
    t3_pass = gap_production > 0.9 * gap_max
    details["gap_production"] = round(float(gap_production), 6)
    details["gap_max"] = round(gap_max, 6)
    if t3_pass:
        passed += 1

    # ── Test 4: Tier-1 identity F + ω = 1 holds everywhere ──
    # F + ω = 1 by construction, but verify no numerical violation
    duality_errors = []
    LE_samples = np.linspace(10, 1500, 50)
    for LE in LE_samples:
        L = LE * DUNE_PEAK_ENERGY_GEV
        P_row = [oscillation_probability_vacuum(1, b, L, DUNE_PEAK_ENERGY_GEV) for b in range(3)]
        k = _oscillation_to_kernel(P_row)
        err = abs(k["F"] + k["omega"] - 1.0)
        duality_errors.append(err)
    max_duality_err = max(duality_errors)
    t4_pass = max_duality_err < 1e-10
    details["max_duality_error"] = f"{max_duality_err:.2e}"
    if t4_pass:
        passed += 1

    # ── Test 5: IC ≤ F everywhere (integrity bound) ──
    ic_leq_f = all(ic <= f + 1e-10 for ic, f in zip(sweep.IC_values, sweep.F_values, strict=True))
    t5_pass = ic_leq_f
    details["IC_leq_F_everywhere"] = ic_leq_f
    if t5_pass:
        passed += 1

    # ── Test 6: Oscillation period matches kernel τ_R ──
    # Atmospheric oscillation period: L/E = π / (1.267 Δm²₃₂)
    dm2_atm = abs(DM2_32)
    expected_period = math.pi / (OSCILLATION_FACTOR * dm2_atm)
    measured_period = sweep.tau_R_period
    period_match = abs(expected_period - measured_period) / expected_period < 0.01
    t6_pass = period_match
    details["expected_period_LE"] = round(expected_period, 2)
    details["measured_period_LE"] = round(measured_period, 2)
    if t6_pass:
        passed += 1

    # ── Test 7: CP violation creates ν/ν̄ kernel asymmetry ──
    # At DUNE baseline, P(ν_μ→ν_e) ≠ P(ν̄_μ→ν̄_e)
    L = DUNE_BASELINE_KM
    E = DUNE_PEAK_ENERGY_GEV
    P_mue_nu = oscillation_probability_vacuum(1, 0, L, E)
    P_mue_nubar = oscillation_probability_vacuum(1, 0, L, E, antineutrino=True)
    cp_asymmetry = abs(P_mue_nu - P_mue_nubar)

    # Kernel the ν and ν̄ rows
    P_nu_row = [oscillation_probability_vacuum(1, b, L, E) for b in range(3)]
    P_nubar_row = [oscillation_probability_vacuum(1, b, L, E, antineutrino=True) for b in range(3)]
    k_nu = _oscillation_to_kernel(P_nu_row)
    k_nubar = _oscillation_to_kernel(P_nubar_row)
    ic_asymmetry = abs(k_nu["IC"] - k_nubar["IC"])

    # CP violation should create detectable asymmetry in IC
    t7_pass = cp_asymmetry > 1e-4 and ic_asymmetry > 1e-6
    details["P_mue_nu"] = round(P_mue_nu, 6)
    details["P_mue_nubar"] = round(P_mue_nubar, 6)
    details["CP_asymmetry"] = round(cp_asymmetry, 6)
    details["IC_asymmetry_nu_nubar"] = round(ic_asymmetry, 8)
    if t7_pass:
        passed += 1

    # ── Test 8: DUNE operating point sits near first oscillation max ──
    # At L=1285 km, E=2.5 GeV → L/E = 514 km/GeV
    # First atmospheric oscillation maximum at L/E ≈ π/(2×1.267×Δm²₃₂) ≈ 494
    dune_LE = L / E
    first_max_LE = math.pi / (2 * OSCILLATION_FACTOR * dm2_atm)
    # DUNE should be within 20% of first maximum
    t8_pass = abs(dune_LE - first_max_LE) / first_max_LE < 0.20
    details["DUNE_LE"] = round(dune_LE, 1)
    details["first_max_LE"] = round(first_max_LE, 1)
    details["proximity_to_max"] = round(abs(dune_LE - first_max_LE) / first_max_LE, 4)
    if t8_pass:
        passed += 1

    return TheoremResult(
        name="T11: Neutrino Oscillation as Periodic Channel Drift",
        statement=(
            "Flavor oscillation P(ν_α→ν_β; L/E) traces a periodic path in kernel space: "
            "F=1/3 constant, IC oscillates, Δ maximal at production, τ_R = oscillation period"
        ),
        n_tests=total_tests,
        n_passed=passed,
        n_failed=total_tests - passed,
        details=details,
        verdict="PROVEN" if passed == total_tests else "FALSIFIED",
    )


# ═══════════════════════════════════════════════════════════════════
# THEOREM T12: MATTER-ENHANCED MIXING AS REGIME TRANSITION
# ═══════════════════════════════════════════════════════════════════


def theorem_T12_matter_enhanced_mixing() -> TheoremResult:
    """T12: Matter-Enhanced Mixing (MSW) as Kernel Regime Transition.

    STATEMENT:
      The MSW (Mikheyev-Smirnov-Wolfenstein) matter effect modifies
      neutrino oscillation probabilities in dense matter, and this
      modification manifests in the GCD kernel as:
        (a) Matter enhances P(ν_μ→ν_e) for neutrinos in normal ordering
        (b) The enhancement shifts the kernel's IC at the detection point
        (c) The ν/ν̄ asymmetry is larger in matter than in vacuum
            (matter breaks the CP symmetry additionally)
        (d) At MSW resonance density, the kernel heterogeneity gap
            reaches a local minimum (resonant mixing → democratic channels)

    WHY THIS MATTERS:
      DUNE relies on matter effects to determine the neutrino mass ordering.
      In the GCD framework, matter effects are a regime-modifying perturbation:
      the constant-density Earth mantle acts as an external coupling (curvature)
      that shifts the oscillation trajectory through kernel space.
      The mass ordering determines whether neutrinos or antineutrinos
      experience resonant enhancement — a binary regime classification
      that maps directly to the kernel's stance.
    """
    total_tests = 5
    passed = 0
    details: dict[str, Any] = {}

    L = DUNE_BASELINE_KM
    E = DUNE_PEAK_ENERGY_GEV

    # ── Test 1: Matter enhances ν_μ→ν_e for neutrinos (normal ordering) ──
    P_vac = oscillation_probability_vacuum(1, 0, L, E)
    P_mat = oscillation_probability_matter(1, 0, L, E)
    enhancement = P_mat / P_vac if P_vac > 1e-10 else 1.0
    # For NO, matter should enhance ν_μ→ν_e (enhancement > 1)
    t1_pass = enhancement > 1.0
    details["P_mue_vacuum"] = round(P_vac, 6)
    details["P_mue_matter"] = round(P_mat, 6)
    details["matter_enhancement"] = round(enhancement, 4)
    if t1_pass:
        passed += 1

    # ── Test 2: Matter suppresses ν̄_μ→ν̄_e for antineutrinos (NO) ──
    P_vac_bar = oscillation_probability_vacuum(1, 0, L, E, antineutrino=True)
    P_mat_bar = oscillation_probability_matter(1, 0, L, E, antineutrino=True)
    suppression = P_mat_bar / P_vac_bar if P_vac_bar > 1e-10 else 1.0
    # For NO, matter should suppress ν̄_μ→ν̄_e (suppression < 1)
    t2_pass = suppression < 1.0
    details["P_mue_bar_vacuum"] = round(P_vac_bar, 6)
    details["P_mue_bar_matter"] = round(P_mat_bar, 6)
    details["antineutrino_suppression"] = round(suppression, 4)
    if t2_pass:
        passed += 1

    # ── Test 3: Matter CP asymmetry larger than vacuum CP asymmetry ──
    A_CP_vac = abs(P_vac - P_vac_bar) / max(P_vac + P_vac_bar, 1e-10)
    A_CP_mat = abs(P_mat - P_mat_bar) / max(P_mat + P_mat_bar, 1e-10)
    t3_pass = A_CP_mat > A_CP_vac
    details["A_CP_vacuum"] = round(A_CP_vac, 6)
    details["A_CP_matter"] = round(A_CP_mat, 6)
    if t3_pass:
        passed += 1

    # ── Test 4: Kernel IC differs between vacuum and matter ──
    P_mu_vac_row = [oscillation_probability_vacuum(1, b, L, E) for b in range(3)]
    P_mu_mat_row_list = []
    for b in range(3):
        if b == 0:
            P_mu_mat_row_list.append(P_mat)
        else:
            P_mu_mat_row_list.append(oscillation_probability_vacuum(1, b, L, E))

    k_vac = _oscillation_to_kernel(P_mu_vac_row)
    k_mat = _oscillation_to_kernel(P_mu_mat_row_list)
    ic_diff = abs(k_vac["IC"] - k_mat["IC"])
    t4_pass = ic_diff > 1e-6
    details["IC_vacuum"] = round(k_vac["IC"], 6)
    details["IC_matter"] = round(k_mat["IC"], 6)
    details["IC_shift_matter"] = round(ic_diff, 8)
    if t4_pass:
        passed += 1

    # ── Test 5: Mass ordering sensitivity — P(NO) ≠ P(IO) ──
    P_mue_NO = oscillation_probability_vacuum(1, 0, L, E)
    P_mue_IO = oscillation_probability_vacuum(1, 0, L, E, dm2_32=-abs(DM2_32))
    delta_P = abs(P_mue_NO - P_mue_IO)
    t5_pass = delta_P > 0.001  # Detectable difference
    details["P_mue_NO"] = round(P_mue_NO, 6)
    details["P_mue_IO"] = round(P_mue_IO, 6)
    details["mass_ordering_sensitivity"] = round(delta_P, 6)
    details["ordering_resolvable"] = delta_P > 0.01
    if t5_pass:
        passed += 1

    return TheoremResult(
        name="T12: Matter-Enhanced Mixing as Kernel Regime Transition",
        statement=(
            "MSW matter effect shifts oscillation kernel: enhancement for ν (NO), "
            "suppression for ν̄, enlarged CP asymmetry, resolvable mass ordering"
        ),
        n_tests=total_tests,
        n_passed=passed,
        n_failed=total_tests - passed,
        details=details,
        verdict="PROVEN" if passed == total_tests else "FALSIFIED",
    )


# ═══════════════════════════════════════════════════════════════════
# MASTER EXECUTION
# ═══════════════════════════════════════════════════════════════════


def run_all_theorems() -> list[TheoremResult]:
    """Execute neutrino oscillation theorems T11–T12."""
    theorems = [
        ("T11", theorem_T11_neutrino_oscillation_drift),
        ("T12", theorem_T12_matter_enhanced_mixing),
    ]
    results = []
    for _label, func in theorems:
        import time

        t0 = time.perf_counter()
        result = func()
        dt = time.perf_counter() - t0
        result.details["time_ms"] = round(dt * 1000, 1)
        results.append(result)
    return results


# ═══════════════════════════════════════════════════════════════════
# SELF-TEST
# ═══════════════════════════════════════════════════════════════════

if __name__ == "__main__":
    print("╔══════════════════════════════════════════════════════════════════════════════════════╗")
    print("║  NEUTRINO OSCILLATION CLOSURE — Flavor Change as Periodic Channel Drift            ║")
    print("║  DUNE/LBNF: 1285 km baseline, ν_μ → ν_e appearance                                ║")
    print("╚══════════════════════════════════════════════════════════════════════════════════════╝")

    # ── PMNS matrix ──
    pmns = compute_pmns_mixing()
    print("\nPMNS Matrix |U_αi|:")
    labels_r = ["e", "μ", "τ"]
    for i, row in enumerate(pmns.U_matrix):
        print(f"  ν_{labels_r[i]}: " + "  ".join(f"{v:.6f}" for v in row))

    # ── DUNE prediction ──
    print("\n── DUNE/LBNF Prediction ──")
    dune = compute_dune_prediction()
    print(f"  Baseline: {dune.baseline_km} km  |  Energy: {dune.peak_energy_GeV} GeV")
    print(f"  P(ν_μ→ν_e) vacuum: {dune.P_mue_vacuum:.6f}  matter: {dune.P_mue_matter:.6f}")
    print(f"  P(ν_μ→ν_μ) vacuum: {dune.P_mumu_vacuum:.6f}  matter: {dune.P_mumu_matter:.6f}")
    print(f"  CP asymmetry:  vacuum={dune.A_CP_vacuum:.4f}  matter={dune.A_CP_matter:.4f}")
    print(f"  Matter enhancement: {dune.matter_enhancement:.4f}")
    print(f"  Mass ordering ΔP: {dune.delta_P_ordering:.6f} → {dune.ordering_verdict}")
    print("\n  Kernel at production (pure ν_μ):")
    print(f"    F = {dune.F_production:.6f}  IC = {dune.IC_production:.6f}  Δ = {dune.gap_production:.6f}")
    print("  Kernel at detection (1285 km):")
    print(f"    F = {dune.F_detection:.6f}  IC = {dune.IC_detection:.6f}  Δ = {dune.gap_detection:.6f}")

    # ── Oscillation sweep ──
    print("\n── Oscillation Sweep (ν_μ) ──")
    sweep = compute_oscillation_sweep(n_points=200)
    print(f"  L/E range: {sweep.L_over_E_values[0]}–{sweep.L_over_E_values[-1]} km/GeV")
    print(f"  F range: [{sweep.F_min}, {sweep.F_max}]")
    print(f"  IC range: [{sweep.IC_min}, {sweep.IC_max}]")
    print(f"  Gap range: [{sweep.gap_min}, {sweep.gap_max}]")
    print(f"  τ_R (first return): {sweep.tau_R_first} km/GeV")
    print(f"  τ_R (period): {sweep.tau_R_period} km/GeV")
    print(f"  Regime counts: Stable={sweep.n_stable}  Watch={sweep.n_watch}  Collapse={sweep.n_collapse}")

    # ── Theorems ──
    print("\n── Theorems ──")
    results = run_all_theorems()
    for r in results:
        icon = "✓" if r.verdict == "PROVEN" else "✗"
        print(f"\n  {icon} {r.name}")
        print(f"    Statement: {r.statement}")
        print(f"    Tests: {r.n_passed}/{r.n_tests} passed → {r.verdict}")
        for key, val in r.details.items():
            if key == "time_ms":
                continue
            print(f"    {key}: {val}")

    total_proven = sum(1 for r in results if r.verdict == "PROVEN")
    total_tests = sum(r.n_tests for r in results)
    total_passed = sum(r.n_passed for r in results)
    print(f"\n  TOTAL: {total_proven}/2 theorems proven, {total_passed}/{total_tests} tests passed")
