//! Structural Proper Time — a new form of agentic time.
//!
//! The idea, in one line: **time is the arc length of a system's worldline
//! through its own state manifold**, not a background coordinate.
//!
//! In relativity, proper time is the length a worldline accumulates as it moves
//! through spacetime. Here we keep the *geometry* and drop the *background*: an
//! agent (or a quantum machine, or a sensor stream) traces a path through a
//! state space, and its **structural proper time** is the accumulated,
//! metric-weighted change along that path. The composite metric is exactly the
//! five-channel form
//!
//! ```text
//!   τ = f(ΔS, Δv, ΔG, ΔC, ΔE)
//! ```
//!
//! * `ΔS` — entropy change,
//! * `Δv` — vector / embedding movement,
//! * `ΔG` — graph topology change,
//! * `ΔC` — coherence change,
//! * `ΔE` — prediction-error change.
//!
//! The system stops asking "what happened at 12:01?" and asks "how much did
//! reality move?". A quiet hour with no state change adds ≈ 0 internal time; one
//! contradiction or boundary collapse is a large jump.
//!
//! This generalizes the three physics clocks of this crate (entropic time uses
//! `ΔS`; thermal time uses modular flow generated by the state; structural time
//! uses the whole manifold) and ships with an honest three-clock benchmark
//! comparing wall, entropy, and structural time on early-warning lead, history
//! compression, and causal-order preservation.

/// A snapshot of a system's structural state at one observation.
#[derive(Clone, Debug)]
pub struct StateSnapshot {
    /// Geometry of the state (belief embedding, syndrome vector, boundary…). `Δv`
    pub embedding: Vec<f64>,
    /// Information content of the state in nats. `ΔS`
    pub entropy: f64,
    /// Coherence / order parameter in `[0, 1]` (1 = fully coherent/healthy). `ΔC`
    pub coherence: f64,
    /// Scalar summary of graph/boundary topology (e.g. mincut value, edge mass). `ΔG`
    pub graph: f64,
    /// Model prediction error / surprise at this state. `ΔE`
    pub pred_error: f64,
}

impl StateSnapshot {
    /// Full constructor with all five channels.
    pub fn full(
        embedding: Vec<f64>,
        entropy: f64,
        coherence: f64,
        graph: f64,
        pred_error: f64,
    ) -> Self {
        StateSnapshot {
            embedding,
            entropy,
            coherence,
            graph,
            pred_error,
        }
    }

    /// Convenience constructor for the common embedding+entropy+coherence case
    /// (graph and prediction-error channels default to zero).
    pub fn new(embedding: Vec<f64>, entropy: f64, coherence: f64) -> Self {
        StateSnapshot::full(embedding, entropy, coherence, 0.0, 0.0)
    }
}

fn l2(a: &[f64], b: &[f64]) -> f64 {
    a.iter()
        .zip(b)
        .map(|(x, y)| (x - y) * (x - y))
        .sum::<f64>()
        .sqrt()
}

/// A clock assigns an internal-time *increment* to a transition between two
/// snapshots. Increments are non-negative, so cumulative internal time is
/// monotone and preserves causal order.
pub trait Clock {
    fn name(&self) -> &str;
    fn tick(&self, prev: &StateSnapshot, cur: &StateSnapshot) -> f64;

    /// Cumulative internal time over a trajectory (length `traj.len()`, starting
    /// at 0 for the first snapshot).
    fn cumulative(&self, traj: &[StateSnapshot]) -> Vec<f64> {
        let mut out = Vec::with_capacity(traj.len());
        let mut acc = 0.0;
        for (i, s) in traj.iter().enumerate() {
            if i > 0 {
                acc += self.tick(&traj[i - 1], s).max(0.0);
            }
            out.push(acc);
        }
        out
    }

    /// Per-step internal-time increments (length `traj.len()`, first is 0).
    fn increments(&self, traj: &[StateSnapshot]) -> Vec<f64> {
        let mut out = vec![0.0];
        for i in 1..traj.len() {
            out.push(self.tick(&traj[i - 1], &traj[i]).max(0.0));
        }
        out
    }
}

/// Wall-clock time: every step is worth exactly one tick, regardless of what
/// happened. The null hypothesis.
pub struct WallClock;
impl Clock for WallClock {
    fn name(&self) -> &str {
        "wall"
    }
    fn tick(&self, _prev: &StateSnapshot, _cur: &StateSnapshot) -> f64 {
        1.0
    }
}

/// Entropic clock: internal time advances with the magnitude of entropy change.
pub struct EntropyClock;
impl Clock for EntropyClock {
    fn name(&self) -> &str {
        "entropy"
    }
    fn tick(&self, prev: &StateSnapshot, cur: &StateSnapshot) -> f64 {
        (cur.entropy - prev.entropy).abs()
    }
}

/// Weights for the composite structural metric `τ = f(ΔS, Δv, ΔG, ΔC, ΔE)`.
#[derive(Clone, Copy, Debug)]
pub struct StructuralMetric {
    pub w_embedding: f64,  // Δv
    pub w_entropy: f64,    // ΔS
    pub w_graph: f64,      // ΔG
    pub w_coherence: f64,  // ΔC (coherence *loss* only)
    pub w_pred_error: f64, // ΔE
    /// Increments below this gate are treated as idle (no internal time).
    pub gate: f64,
}

impl Default for StructuralMetric {
    fn default() -> Self {
        StructuralMetric {
            w_embedding: 1.0,
            w_entropy: 1.0,
            w_graph: 0.5,
            w_coherence: 1.0,
            w_pred_error: 0.5,
            gate: 0.0,
        }
    }
}

/// Structural Proper Time: internal time = composite structural arc length.
pub struct StructuralProperTime {
    pub metric: StructuralMetric,
}

impl StructuralProperTime {
    pub fn new(metric: StructuralMetric) -> Self {
        StructuralProperTime { metric }
    }
}

impl Clock for StructuralProperTime {
    fn name(&self) -> &str {
        "structural"
    }
    fn tick(&self, prev: &StateSnapshot, cur: &StateSnapshot) -> f64 {
        let m = &self.metric;
        let d_embed = l2(&prev.embedding, &cur.embedding); // Δv
        let d_entropy = (cur.entropy - prev.entropy).abs(); // ΔS
        let d_graph = (cur.graph - prev.graph).abs(); // ΔG
        let coherence_loss = (prev.coherence - cur.coherence).max(0.0); // ΔC
        let d_pred = (cur.pred_error - prev.pred_error).abs(); // ΔE
        let raw = m.w_embedding * d_embed
            + m.w_entropy * d_entropy
            + m.w_graph * d_graph
            + m.w_coherence * coherence_loss
            + m.w_pred_error * d_pred;
        if raw < m.gate {
            0.0
        } else {
            raw
        }
    }
}

/// First step at which a clock's instantaneous rate exceeds `mean + k·std` of
/// its baseline rates (computed over the first `baseline_window` steps). `None`
/// if it never fires.
pub fn alarm_step(
    clock: &dyn Clock,
    traj: &[StateSnapshot],
    baseline_window: usize,
    k_sigma: f64,
) -> Option<usize> {
    let inc = clock.increments(traj);
    if traj.len() <= baseline_window + 1 {
        return None;
    }
    let base: Vec<f64> = inc[1..=baseline_window].to_vec();
    let mean = base.iter().sum::<f64>() / base.len() as f64;
    let var = base.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / base.len() as f64;
    let std = var.sqrt();
    let threshold = mean + k_sigma * std;
    for i in (baseline_window + 1)..traj.len() {
        if inc[i] > threshold {
            return Some(i);
        }
    }
    None
}

/// Early-warning lead time: steps between the alarm and the failure. `0` if the
/// clock never raises an alarm before failure.
pub fn early_warning_lead(
    clock: &dyn Clock,
    traj: &[StateSnapshot],
    fail_index: usize,
    baseline_window: usize,
    k_sigma: f64,
) -> usize {
    match alarm_step(clock, traj, baseline_window, k_sigma) {
        Some(a) if a <= fail_index => fail_index - a,
        _ => 0,
    }
}

/// Choose `budget` keyframe indices spaced evenly in a clock's internal time,
/// always including the first and last step.
pub fn keyframes(clock: &dyn Clock, traj: &[StateSnapshot], budget: usize) -> Vec<usize> {
    let n = traj.len();
    if budget >= n || budget < 2 {
        return (0..n).collect();
    }
    let cum = clock.cumulative(traj);
    let total = *cum.last().unwrap();
    let mut frames = Vec::with_capacity(budget);
    if total <= 0.0 {
        for j in 0..budget {
            frames.push(j * (n - 1) / (budget - 1));
        }
        frames.dedup();
        return frames;
    }
    for j in 0..budget {
        let level = total * j as f64 / (budget - 1) as f64;
        let mut best = 0usize;
        let mut bestd = f64::INFINITY;
        for (i, &c) in cum.iter().enumerate() {
            let d = (c - level).abs();
            if d < bestd {
                bestd = d;
                best = i;
            }
        }
        frames.push(best);
    }
    frames.sort_unstable();
    frames.dedup();
    if frames[0] != 0 {
        frames.insert(0, 0);
    }
    if *frames.last().unwrap() != n - 1 {
        frames.push(n - 1);
    }
    frames
}

/// Reconstruct the embedding trajectory by linear interpolation between
/// keyframes (in wall index) and return the maximum L2 reconstruction error.
/// Lower is better compression at a fixed keyframe budget.
pub fn compression_error(clock: &dyn Clock, traj: &[StateSnapshot], budget: usize) -> f64 {
    let frames = keyframes(clock, traj, budget);
    let mut max_err = 0.0f64;
    for i in 0..traj.len() {
        let mut lo = frames[0];
        let mut hi = *frames.last().unwrap();
        for w in frames.windows(2) {
            if w[0] <= i && i <= w[1] {
                lo = w[0];
                hi = w[1];
                break;
            }
        }
        let recon: Vec<f64> = if hi == lo {
            traj[lo].embedding.clone()
        } else {
            let alpha = (i - lo) as f64 / (hi - lo) as f64;
            traj[lo]
                .embedding
                .iter()
                .zip(&traj[hi].embedding)
                .map(|(a, b)| a + alpha * (b - a))
                .collect()
        };
        max_err = max_err.max(l2(&recon, &traj[i].embedding));
    }
    max_err
}

/// Smallest keyframe budget for which the clock's reconstruction error falls to
/// `tol` or below. Returns `traj.len()` if the tolerance is never met. The ratio
/// of two clocks' results is a direct "history compression" factor: how many
/// fewer samples structural time needs to preserve the trajectory to tolerance.
pub fn samples_to_tolerance(clock: &dyn Clock, traj: &[StateSnapshot], tol: f64) -> usize {
    let n = traj.len();
    for budget in 2..=n {
        if compression_error(clock, traj, budget) <= tol {
            return budget;
        }
    }
    n
}

/// Agent "stuck" efficiency: external progress per unit of structural time
/// (internal churn). High structural time with little progress means the agent
/// is thrashing — lots of internal effort, no real movement.
pub fn progress_efficiency(structural_time_spent: f64, progress: f64) -> f64 {
    if structural_time_spent <= 1e-12 {
        f64::INFINITY
    } else {
        progress / structural_time_spent
    }
}

/// Replanning trigger: fire when progress per unit structural time drops below
/// `threshold`. This replaces the usual "N steps with no result" heuristic with
/// "lots of internal change but no convergence" — the agent decides to replan
/// based on *state movement*, not wall-clock or step count.
pub fn should_replan(structural_time_spent: f64, progress: f64, threshold: f64) -> bool {
    progress_efficiency(structural_time_spent, progress) < threshold
}

/// Whether cumulative internal time is monotone non-decreasing (causal order
/// preserved). True for any clock with non-negative ticks.
pub fn preserves_causal_order(clock: &dyn Clock, traj: &[StateSnapshot]) -> bool {
    let cum = clock.cumulative(traj);
    cum.windows(2).all(|w| w[1] + 1e-12 >= w[0])
}

/// A compact report for one clock on one trajectory.
#[derive(Clone, Debug)]
pub struct ClockReport {
    pub name: String,
    pub lead: usize,
    pub compression_error: f64,
    pub causal_order_ok: bool,
}

/// Benchmark a clock end to end.
pub fn evaluate(
    clock: &dyn Clock,
    traj: &[StateSnapshot],
    fail_index: usize,
    baseline_window: usize,
    k_sigma: f64,
    budget: usize,
) -> ClockReport {
    ClockReport {
        name: clock.name().to_string(),
        lead: early_warning_lead(clock, traj, fail_index, baseline_window, k_sigma),
        compression_error: compression_error(clock, traj, budget),
        causal_order_ok: preserves_causal_order(clock, traj),
    }
}

// ---------------------------------------------------------------------------
// Synthetic scenario generator (deterministic; no external RNG dependency).
// ---------------------------------------------------------------------------

/// Deterministic xorshift64* PRNG so the benchmark is reproducible.
struct Rng(u64);
impl Rng {
    fn new(seed: u64) -> Self {
        Rng(seed | 1)
    }
    fn next_f64(&mut self) -> f64 {
        let mut x = self.0;
        x ^= x >> 12;
        x ^= x << 25;
        x ^= x >> 27;
        self.0 = x;
        let v = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
        ((v >> 11) as f64 / (1u64 << 53) as f64) * 2.0 - 1.0
    }
}

/// Parameters of the drift-to-failure scenario.
#[derive(Clone, Copy, Debug)]
pub struct Scenario {
    pub dim: usize,
    pub steps: usize,
    /// Steps used to learn each clock's quiet baseline (must precede `embed_onset`).
    pub baseline_window: usize,
    /// First *structural* regime transition — the precursor that moves before
    /// any thermodynamic signal.
    pub embed_onset: usize,
    /// Step where *entropy* begins to rise (a later regime transition: systems
    /// fail structurally before they fail visibly).
    pub entropy_onset: usize,
    /// Step at which visible failure occurs.
    pub fail_index: usize,
    pub noise: f64,
    pub seed: u64,
}

impl Default for Scenario {
    fn default() -> Self {
        Scenario {
            dim: 8,
            steps: 100,
            baseline_window: 18,
            embed_onset: 22,
            entropy_onset: 60,
            fail_index: 80,
            noise: 0.01,
            seed: 0xC0FFEE,
        }
    }
}

/// Ramp contribution of a transition centered at `center` (width 4) evaluated
/// at step `i`: 0 before, 1 after, linear across the transition.
fn ramp(i: usize, center: usize) -> f64 {
    let half = 2.0;
    let x = (i as f64 - (center as f64 - half)) / (2.0 * half);
    x.clamp(0.0, 1.0)
}

/// Generate a **regime-transition** drift-to-failure trajectory: long quiet
/// plateaus punctuated by discrete boundary crossings that march the system
/// toward a failure attractor. Structure (embedding/graph/coherence) moves at
/// the early transitions; entropy only rises at the later ones. This is the
/// variable-rate, bursty regime where internal time genuinely beats wall time —
/// both for early warning (structure precedes the entropy signal) and for
/// compression (sample the transitions, skip the plateaus).
pub fn generate_scenario(sc: &Scenario) -> Vec<StateSnapshot> {
    let mut rng = Rng::new(sc.seed);
    let attractor: Vec<f64> = (0..sc.dim)
        .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
        .collect();

    // Four regime transitions, each advancing the system by a quarter toward
    // the failure attractor.
    let t1 = sc.embed_onset;
    let t3 = sc.entropy_onset;
    let t4 = sc.fail_index.saturating_sub(3);
    let t2 = (t1 + t3) / 2;
    let centers = [t1, t2, t3, t4];

    let mut traj = Vec::with_capacity(sc.steps);
    for i in 0..sc.steps {
        // Accumulated regime level in [0, 1].
        let level: f64 = centers.iter().map(|&c| 0.25 * ramp(i, c)).sum::<f64>();

        // Embedding marches toward the attractor as the level rises.
        let embedding: Vec<f64> = attractor
            .iter()
            .map(|&a| level * a + sc.noise * rng.next_f64())
            .collect();

        // Entropy only responds to the *late* regime (level past 0.5), i.e. it
        // lags the structural signal.
        let entropy =
            0.2 + 1.8 * ((level - 0.5) / 0.5).clamp(0.0, 1.0) + sc.noise * rng.next_f64().abs();
        // Coherence and graph topology track the structural level directly.
        let coherence = (1.0 - level).clamp(0.0, 1.0);
        let graph = level + sc.noise * rng.next_f64().abs();
        // Prediction error spikes only in the final approach (level past 0.75).
        let pred_error = ((level - 0.75) / 0.25).clamp(0.0, 1.0);

        traj.push(StateSnapshot::full(
            embedding, entropy, coherence, graph, pred_error,
        ));
    }
    traj
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ticks_non_negative_and_order_preserved() {
        let sc = Scenario::default();
        let traj = generate_scenario(&sc);
        let spt = StructuralProperTime::new(StructuralMetric::default());
        assert!(preserves_causal_order(&spt, &traj));
        assert!(preserves_causal_order(&EntropyClock, &traj));
        assert!(preserves_causal_order(&WallClock, &traj));
    }

    #[test]
    fn structural_warns_earlier_than_entropy_and_wall() {
        let sc = Scenario::default();
        let traj = generate_scenario(&sc);
        let spt = StructuralProperTime::new(StructuralMetric::default());
        let bw = sc.baseline_window;

        let lead_wall = early_warning_lead(&WallClock, &traj, sc.fail_index, bw, 4.0);
        let lead_entropy = early_warning_lead(&EntropyClock, &traj, sc.fail_index, bw, 4.0);
        let lead_struct = early_warning_lead(&spt, &traj, sc.fail_index, bw, 4.0);

        // Wall time gives no structural early warning.
        assert_eq!(lead_wall, 0);
        // Structure precedes the thermodynamic (entropy) signal.
        assert!(
            lead_struct > lead_entropy,
            "structural lead {lead_struct} should exceed entropy lead {lead_entropy}"
        );
        // Acceptance target: ≥ 2x the entropy clock's lead.
        assert!(
            lead_struct as f64 >= 2.0 * lead_entropy.max(1) as f64,
            "structural lead {lead_struct} should be >= 2x entropy lead {lead_entropy}"
        );
    }

    #[test]
    fn structural_compresses_better_than_wall() {
        let sc = Scenario::default();
        let traj = generate_scenario(&sc);
        let spt = StructuralProperTime::new(StructuralMetric::default());
        let budget = 10;
        let err_wall = compression_error(&WallClock, &traj, budget);
        let err_struct = compression_error(&spt, &traj, budget);
        assert!(
            err_struct < err_wall,
            "structural error {err_struct} should beat wall error {err_wall}"
        );
    }

    #[test]
    fn structural_needs_fewer_samples_for_tolerance() {
        let sc = Scenario::default();
        let traj = generate_scenario(&sc);
        let spt = StructuralProperTime::new(StructuralMetric::default());
        let tol = 0.3;
        let wall_budget = samples_to_tolerance(&WallClock, &traj, tol);
        let struct_budget = samples_to_tolerance(&spt, &traj, tol);
        assert!(
            struct_budget < wall_budget,
            "structural needs {struct_budget} samples, wall needs {wall_budget}"
        );
    }

    #[test]
    fn replans_on_low_structural_efficiency() {
        // Lots of internal churn, little progress -> replan.
        assert!(should_replan(5.0, 0.1, 0.1));
        // Healthy progress per unit churn -> keep going.
        assert!(!should_replan(1.0, 0.9, 0.1));
        // No churn at all -> not stuck (infinite efficiency).
        assert!(!should_replan(0.0, 0.0, 0.1));
    }
}
