//! # tick_lag — `TickLagGauge`: the ADR-0010 doctrine-drift detector
//!
//! See spec §3.6 + §6.4 for the design. Surfaces `tick_overruns` +
//! `last_tick_micros` (here: `max_tick_lag_micros`) as primary readouts
//! in the `SweepReport`. The concurrency sweep turns the gauge from a
//! counter into a decision artifact: "does single-thread poll loop
//! breach budget at realistic concurrency?" gets answered with data,
//! not vibes. If yes, the dedicated threadpool-shard graduation (slice-4
//! deferral #2) gets its data-driven case.
//!
//! # Standalone-path adaptation (spec §3.6 deviation)
//!
//! Spec §3.6 says the gauge "polls `MediaCmd::Stats` during the sweep"
//! — the slice-5/seams `MediaStats { tick_overruns, last_tick_micros }`
//! readout from the binary's `MediaThread`. S4's standalone-path
//! conclusion (per the plan + kickoff hard rule) means the SimCall
//! wires itself in tokio WITHOUT registering with the binary's
//! `MediaThread` — no `MediaCmd::Stats` channel exists to poll.
//!
//! This S6 implementation adapts: the gauge is wired INTO the
//! `SimCall`'s tick loop directly (via a shared `Arc<TickLagStats>`
//! handle). `SimCall::run_with_gauge` records per-tick wall-clock
//! duration via `Instant::now()` measurement around the tick work.
//! The semantics are equivalent (max tick lag + overrun count + pct),
//! the source is the in-process tokio SimCall tick loop rather than
//! the binary's `MediaThread` poll loop. A future slice (post-spearhead
//! refinement, paired with network-realism mode) wires the gauge
//! against the binary's `MediaThread` per spec §3.6 — requires either
//! `MediaThread` registration (the RegisterSim variant forbidden this
//! slice) OR a client-server sim mode (deferred per spec §8.6).
//!
//! # Hot-path design: atomics (not `Mutex<Vec<Duration>>`)
//!
//! Per-tick recording is 3 atomic ops:
//! 1. CAS loop on `max_tick_lag_micros` (atomic max update).
//! 2. Conditional `fetch_add` on `tick_overruns` if the tick exceeded 10 ms.
//! 3. Unconditional `fetch_add` on `total_ticks`.
//!
//! A `Mutex<Vec<Duration>>` would lock the vector per tick — locking
//! overhead is significant relative to the per-tick work budget
//! (microseconds). Atomics keep the per-tick critical section lock-free.

use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;

/// Threshold for "tick overrun" (per spec §5.1 `TICK_LAG_MAX_MS = 10.0`).
/// 10 ms = 10_000 µs. A tick whose wall-clock duration exceeds this is
/// an overrun (the meta-tick's nominal period was breached).
const TICK_LAG_OVERRUN_THRESHOLD_US: u64 = 10_000;

/// Atomic accumulators for tick-lag measurements during a concurrency
/// sweep. Shared between N `SimCall`s (cloned via `Arc`) + read
/// post-sweep by the `ConcurrencyRunner` / `TickLagGauge`.
///
/// # Why `Arc<Self>` (the constructor returns `Arc<Self>`)
///
/// The stats are shared with N concurrent `SimCall` tasks (one clone
/// per task) + the post-sweep reader. The natural lifetime is "the
/// duration of one concurrency level's sweep" — short, scoped. An
/// `Arc<Self>` from construction avoids a separate `Arc::new(...)`
/// wrapper at every call site (mirrors `ReflexMetrics::new()` in
/// slice-4 + `TapMetrics::new()` in slice-2 — both return `Arc<Self>`).
pub struct TickLagStats {
    max_tick_lag_micros: AtomicU64,
    tick_overruns: AtomicU64,
    total_ticks: AtomicU64,
}

impl TickLagStats {
    /// Construct a fresh `Arc<TickLagStats>` with all counters zeroed.
    pub fn new() -> Arc<Self> {
        Arc::new(Self {
            max_tick_lag_micros: AtomicU64::new(0),
            tick_overruns: AtomicU64::new(0),
            total_ticks: AtomicU64::new(0),
        })
    }

    /// Record a tick's wall-clock duration. Hot path: matches the 3-atomic-ops
    /// budget. Called per-tick by `SimCall::run_with_gauge`.
    ///
    /// # CAS loop on max
    ///
    /// `compare_exchange_weak` is used (not `compare_exchange`) — weak
    /// CAS can spuriously fail, but in a tight update loop the cost is
    /// lower than strong CAS + the spurious-failure retry is bounded
    /// (the next iteration re-reads the current max). The `Ordering::Relaxed`
    /// on both success + failure is intentional: this is a statistics
    /// counter where we don't need cross-thread synchronization ordering
    /// (the post-sweep reader sees a consistent-enough snapshot; the
    /// exact order doesn't matter for "max observed" + "count > 10ms").
    pub fn record_tick(&self, elapsed: Duration) {
        let elapsed_us = elapsed.as_micros() as u64;
        let mut current_max = self.max_tick_lag_micros.load(Ordering::Relaxed);
        while elapsed_us > current_max {
            match self.max_tick_lag_micros.compare_exchange_weak(
                current_max,
                elapsed_us,
                Ordering::Relaxed,
                Ordering::Relaxed,
            ) {
                Ok(_) => break,
                Err(actual) => current_max = actual,
            }
        }
        if elapsed_us > TICK_LAG_OVERRUN_THRESHOLD_US {
            self.tick_overruns.fetch_add(1, Ordering::Relaxed);
        }
        self.total_ticks.fetch_add(1, Ordering::Relaxed);
    }

    pub fn max_tick_lag_micros(&self) -> u64 {
        self.max_tick_lag_micros.load(Ordering::Relaxed)
    }

    pub fn tick_overruns(&self) -> u64 {
        self.tick_overruns.load(Ordering::Relaxed)
    }

    pub fn total_ticks(&self) -> u64 {
        self.total_ticks.load(Ordering::Relaxed)
    }

    /// Percentage of ticks that exceeded the 10 ms overrun threshold.
    /// Returns 0.0 if no ticks were recorded (avoids div-by-zero).
    pub fn tick_overrun_pct(&self) -> f64 {
        let total = self.total_ticks();
        if total == 0 {
            return 0.0;
        }
        (self.tick_overruns() as f64 / total as f64) * 100.0
    }
}

impl Default for TickLagStats {
    fn default() -> Self {
        Self {
            max_tick_lag_micros: AtomicU64::new(0),
            tick_overruns: AtomicU64::new(0),
            total_ticks: AtomicU64::new(0),
        }
    }
}

/// The read-side API for the gauge. Wraps an `Arc<TickLagStats>` created
/// internally (or an externally-shared one). The `ConcurrencyRunner`
/// creates one gauge per concurrency level + passes the `stats_handle()`
/// to each of the N `SimCall`s via `SimCall::run_with_gauge`.
pub struct TickLagGauge {
    stats: Arc<TickLagStats>,
}

impl TickLagGauge {
    pub fn new() -> Self {
        Self {
            stats: TickLagStats::new(),
        }
    }

    /// Get a stats handle that can be passed to `SimCall::run_with_gauge`
    /// for shared recording during the concurrency sweep.
    pub fn stats_handle(&self) -> Arc<TickLagStats> {
        self.stats.clone()
    }

    pub fn max_tick_lag_micros(&self) -> u64 {
        self.stats.max_tick_lag_micros()
    }

    pub fn tick_overruns(&self) -> u64 {
        self.stats.tick_overruns()
    }

    pub fn total_ticks(&self) -> u64 {
        self.stats.total_ticks()
    }

    pub fn tick_overrun_pct(&self) -> f64 {
        self.stats.tick_overrun_pct()
    }
}

impl Default for TickLagGauge {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn gauge_records_zero_initially() {
        let gauge = TickLagGauge::new();
        assert_eq!(gauge.max_tick_lag_micros(), 0);
        assert_eq!(gauge.tick_overruns(), 0);
        assert_eq!(gauge.total_ticks(), 0);
        assert_eq!(gauge.tick_overrun_pct(), 0.0);
    }

    #[test]
    fn gauge_records_max_tick_lag_across_samples() {
        // Three ticks: 500us, 1500us, 800us. Max should be 1500us.
        // Total ticks should be 3. No overruns (all < 10ms).
        let stats = TickLagStats::new();
        stats.record_tick(Duration::from_micros(500));
        stats.record_tick(Duration::from_micros(1500));
        stats.record_tick(Duration::from_micros(800));
        assert_eq!(stats.max_tick_lag_micros(), 1500);
        assert_eq!(stats.total_ticks(), 3);
        assert_eq!(stats.tick_overruns(), 0);
        assert_eq!(stats.tick_overrun_pct(), 0.0);
    }

    #[test]
    fn gauge_counts_overruns_above_threshold() {
        // 4 ticks: 2 under 10ms, 2 over. Overrun pct = 50%.
        let stats = TickLagStats::new();
        stats.record_tick(Duration::from_micros(5_000)); // under
        stats.record_tick(Duration::from_micros(11_000)); // over
        stats.record_tick(Duration::from_micros(20_000)); // over
        stats.record_tick(Duration::from_micros(7_000)); // under
        assert_eq!(stats.tick_overruns(), 2);
        assert_eq!(stats.total_ticks(), 4);
        assert_eq!(stats.max_tick_lag_micros(), 20_000);
        assert_eq!(stats.tick_overrun_pct(), 50.0);
    }

    #[test]
    fn gauge_handles_zero_total_ticks_pct() {
        // No ticks recorded → pct should return 0.0 (not NaN).
        let stats = TickLagStats::new();
        assert_eq!(stats.tick_overrun_pct(), 0.0);
    }
}
