//! Replication lag gauge: computes per-shard lag in WAL segments.
//!
//! The lag is defined as `leader_seqno - applied_seqno` for each shard.
//! A lag of 0 means the follower is fully caught up. The gauge is designed
//! for integration into the Prometheus metrics endpoint.

use std::sync::atomic::{AtomicU64, Ordering};

use super::{
    shard::ShardId,
    state::{ReplicationState, advance_atomic_max},
};

/// Tracks the latest known leader seqno per shard and computes the lag
/// relative to the local `ReplicationState`.
///
/// # Thread Safety
///
/// Uses `AtomicU64` for the leader seqno. The `ReplicationState` it
/// references is also lock-free. Reading lag is wait-free.
#[derive(Debug)]
pub struct ReplicationLagGauge {
    /// The latest seqno observed from the leader for the primary shard.
    /// In single-shard deployments, this is `ShardId::SINGLE`.
    leader_seqno: AtomicU64,
    /// The local replication state (shared with the receiver).
    state: std::sync::Arc<ReplicationState>,
    /// The shard we are tracking.
    shard_id: ShardId,
}

impl ReplicationLagGauge {
    /// Create a new lag gauge for a single shard.
    #[must_use]
    pub const fn new(shard_id: ShardId, state: std::sync::Arc<ReplicationState>) -> Self {
        Self {
            leader_seqno: AtomicU64::new(0),
            state,
            shard_id,
        }
    }

    /// Update the leader's latest known seqno.
    ///
    /// Monotonic: smaller values are silently ignored. Shares the lock-free
    /// max-into-atomic helper with [`ReplicationState::advance`].
    pub fn update_leader_seqno(&self, seqno: u64) {
        advance_atomic_max(&self.leader_seqno, seqno);
    }

    /// Compute the current lag in WAL segments.
    ///
    /// Returns `leader_seqno - applied_seqno`. If the leader seqno is
    /// unknown (still 0), returns 0.
    ///
    /// # Unknown shard
    ///
    /// If this gauge's `shard_id` is not tracked by the shared
    /// [`ReplicationState`] (a wiring bug — e.g. a gauge built for one shard
    /// against a state that tracks only others), `applied_seqno` is `None`. That
    /// is a misconfiguration, not a legitimately-zero reading, so it is surfaced:
    /// a `debug_assert` fires in debug/test builds and a `warn!` is logged in
    /// release builds (see [`applied_seqno`](Self::applied_seqno)). The lag then
    /// falls back to treating applied as 0 (maximally behind) so a real wiring
    /// bug reads as "far behind" rather than the silent "caught up" it would
    /// otherwise masquerade as.
    #[must_use]
    pub fn lag_segments(&self) -> u64 {
        let leader = self.leader_seqno.load(Ordering::Acquire);
        let applied = self.applied_seqno();
        leader.saturating_sub(applied)
    }

    /// Whether this gauge's shard is actually tracked by the shared
    /// [`ReplicationState`].
    ///
    /// `false` means the gauge was wired against a state that does not track its
    /// shard, so every reading from [`lag_segments`](Self::lag_segments) /
    /// [`applied_seqno`](Self::applied_seqno) is a misconfiguration fallback, not
    /// a real measurement. Health/diagnostics can poll this to distinguish a
    /// genuinely-zero lag from an unwired gauge.
    #[must_use]
    pub fn shard_tracked(&self) -> bool {
        self.state.applied_seqno(self.shard_id).is_some()
    }

    /// The shard this gauge tracks.
    #[must_use]
    pub const fn shard_id(&self) -> ShardId {
        self.shard_id
    }

    /// The latest leader seqno observed.
    #[must_use]
    pub fn leader_seqno(&self) -> u64 {
        self.leader_seqno.load(Ordering::Acquire)
    }

    /// The latest applied seqno on this node.
    ///
    /// Returns 0 if this gauge's shard is not tracked by the shared
    /// [`ReplicationState`] — but that case is a wiring bug, not a real reading,
    /// so it is surfaced loudly (`debug_assert` in debug/test, `warn!` in
    /// release) rather than silently conflated with a true zero. Use
    /// [`shard_tracked`](Self::shard_tracked) to test for it explicitly.
    #[must_use]
    pub fn applied_seqno(&self) -> u64 {
        if let Some(applied) = self.state.applied_seqno(self.shard_id) {
            applied
        } else {
            debug_assert!(
                false,
                "ReplicationLagGauge for shard {} is not tracked by the shared \
                 ReplicationState; lag readings are a misconfiguration fallback, \
                 not a real measurement",
                self.shard_id
            );
            tracing::warn!(
                shard = %self.shard_id,
                "lag gauge shard is not tracked by ReplicationState; reporting \
                 applied=0 (maximally behind) — this is a wiring bug, not a real \
                 zero reading"
            );
            0
        }
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use std::sync::Arc;

    use super::*;

    #[test]
    fn lag_starts_at_zero() {
        let state = Arc::new(ReplicationState::single());
        let gauge = ReplicationLagGauge::new(ShardId::SINGLE, state);
        assert_eq!(gauge.lag_segments(), 0);
    }

    #[test]
    fn lag_reflects_leader_ahead() {
        let state = Arc::new(ReplicationState::single());
        let gauge = ReplicationLagGauge::new(ShardId::SINGLE, Arc::clone(&state));
        gauge.update_leader_seqno(10);
        assert_eq!(gauge.lag_segments(), 10);
        // Apply some segments
        state.advance(ShardId::SINGLE, 7);
        assert_eq!(gauge.lag_segments(), 3);
    }

    #[test]
    fn lag_zero_when_caught_up() {
        let state = Arc::new(ReplicationState::single());
        let gauge = ReplicationLagGauge::new(ShardId::SINGLE, Arc::clone(&state));
        gauge.update_leader_seqno(5);
        state.advance(ShardId::SINGLE, 5);
        assert_eq!(gauge.lag_segments(), 0);
    }

    #[test]
    fn leader_seqno_is_monotone() {
        let state = Arc::new(ReplicationState::single());
        let gauge = ReplicationLagGauge::new(ShardId::SINGLE, state);
        gauge.update_leader_seqno(10);
        gauge.update_leader_seqno(5); // should be ignored
        assert_eq!(gauge.leader_seqno(), 10);
        gauge.update_leader_seqno(15);
        assert_eq!(gauge.leader_seqno(), 15);
    }

    #[test]
    fn accessors_work() {
        let state = Arc::new(ReplicationState::single());
        state.advance(ShardId::SINGLE, 3);
        let gauge = ReplicationLagGauge::new(ShardId::SINGLE, Arc::clone(&state));
        gauge.update_leader_seqno(7);
        assert_eq!(gauge.shard_id(), ShardId::SINGLE);
        assert_eq!(gauge.leader_seqno(), 7);
        assert_eq!(gauge.applied_seqno(), 3);
        assert_eq!(gauge.lag_segments(), 4);
    }

    /// SUGGESTION (repl-shipping): a gauge whose shard is tracked by the shared
    /// state reports `shard_tracked() == true`; one built against a state that
    /// does NOT track its shard reports `false`, so a misconfigured gauge is
    /// distinguishable from a legitimately-zero reading instead of silently
    /// masquerading as "caught up".
    #[test]
    fn shard_tracked_distinguishes_unwired_gauge() {
        // Wired correctly: ShardId::SINGLE is tracked.
        let state = Arc::new(ReplicationState::single());
        let tracked = ReplicationLagGauge::new(ShardId::SINGLE, Arc::clone(&state));
        assert!(
            tracked.shard_tracked(),
            "a gauge whose shard is tracked must report shard_tracked() == true"
        );

        // Wired against a state that tracks only OTHER shards: the gauge's shard
        // (SINGLE) is unknown, so shard_tracked() must report the misconfiguration.
        let other_state = Arc::new(ReplicationState::new(&[ShardId(7), ShardId(8)]));
        let unwired = ReplicationLagGauge::new(ShardId::SINGLE, Arc::clone(&other_state));
        assert!(
            !unwired.shard_tracked(),
            "a gauge built against a state that does not track its shard must report \
             shard_tracked() == false"
        );
    }
}
