"""
Field propagation — the generation heartbeat.

Each step: F <- versor_apply(V, F)

V is the rotor for the current node's outgoing edge in the vocab manifold.
No correction. No normalization. No conditional branching. The loop is tight.

Energy recomputation: after each versor step the EnergyProfile carried in
FieldState is refreshed. The refresh uses only the structural inputs that
are available without external context (activation_count tracks steps taken;
cycle is the new step index). Convergence density and morphology features
are not available inside propagate_step — they are set at the injection gate
and carried forward unchanged. Coherence residual is zero inside a clean
propagation path (no corrective pass is applied here). This is intentional:
propagation is not correction.

Hot path routes through algebra.backend, which dispatches to the Rust
extension (core_rs) when available and falls back to pure Python silently.
"""

from algebra.backend import versor_apply
from core.physics.energy import FieldEnergyOperator
from field.state import FieldState

_energy_op = FieldEnergyOperator()


def propagate_step(state: FieldState, V) -> FieldState:
    """
    Apply one versor transition and refresh the energy profile.

    V is the edge rotor from the current node.
    Returns a new FieldState one step forward on the manifold.

    Energy recomputation policy:
    - activation_count increments by 1 per step (field is actively propagating).
    - current_cycle = new step index (monotonic proxy for time).
    - last_activation_cycle stays at the value set at injection (the gate
      records when this region was first injected; propagation does not reset
      that anchor).
    - coherence_residual = 0.0 (propagation is not a corrective pass).
    - convergence_density and morphology_features are inherited from the
      existing EnergyProfile when one is present; otherwise defaults apply.
    - anchor_adjacent is inherited unchanged.
    """
    new_F = versor_apply(V, state.F)
    new_step = state.step + 1

    if state.energy is not None:
        ep = state.energy
        new_energy = _energy_op.compute(
            convergence_density=ep.convergence_density,
            activation_count=ep.activation_count + 1,
            current_cycle=new_step,
            last_activation_cycle=ep.last_activation_cycle,
            coherence_residual=0.0,
            morphology_features=None,
            anchor_adjacent=ep.anchor_adjacent,
            carry_aspect_weight=ep.aspect_weight,
        )
    else:
        new_energy = None

    return FieldState(
        F=new_F,
        node=state.node,
        step=new_step,
        holonomy=state.holonomy,
        energy=new_energy,
        valence=state.valence,
    )
