//! Hybrid Logical Clock (HLC) for causal ordering across distributed nodes.
//!
//! Reference: Kulkarni et al., 2014 -- "Logical Physical Clocks and Consistent
//! Snapshots in Globally Distributed Databases".
//!
//! The HLC combines a wall-clock component with a bounded logical counter.
//! On every local event the clock advances to `max(wall, prev_wall)`;
//! when the wall clock does not advance, the logical counter increments.
//! On receiving a remote timestamp the clock advances past *both* the local
//! state and the remote timestamp, preserving the causal "happens-before"
//! relation.
//!
//! # Internal representation
//!
//! To guarantee uniqueness under concurrent `now()` calls without a mutex,
//! the mutable state is packed into a single `AtomicU64`:
//!
//! ```text
//!  bits 63..16  (48 bits)  wall-clock milliseconds since UNIX epoch
//!  bits 15..0   (16 bits)  logical counter (0..65535)
//! ```
//!
//! A single `compare_exchange` on this packed word atomically advances both
//! components, eliminating the race window that would exist between two
//! separate atomics.  The public [`HlcTimestamp`] uses nanoseconds for the
//! wall clock (ms * `1_000_000`) and a `u32` logical counter for headroom.

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

use crate::replication::shard::ShardId;

/// Number of bits allocated to the logical counter in the packed word.
const LOGICAL_BITS: u32 = 16;
/// Mask for extracting the logical counter from the packed word.
const LOGICAL_MASK: u64 = (1u64 << LOGICAL_BITS) - 1; // 0xFFFF
/// Maximum logical counter value before saturation.
const LOGICAL_MAX: u64 = LOGICAL_MASK;

/// Pack `(wall_ms, logical)` into a single `u64`.
const fn pack(wall_ms: u64, logical: u16) -> u64 {
    (wall_ms << LOGICAL_BITS) | (logical as u64)
}

/// Unpack a `u64` into `(wall_ms, logical)`.
const fn unpack(packed: u64) -> (u64, u16) {
    let wall_ms = packed >> LOGICAL_BITS;
    #[allow(clippy::cast_possible_truncation)]
    let logical = (packed & LOGICAL_MASK) as u16;
    (wall_ms, logical)
}

// ---------------------------------------------------------------------------
// HlcTimestamp
// ---------------------------------------------------------------------------

/// A point-in-time HLC timestamp.
///
/// Ordering: `(wall_ns, logical, node_id)` -- lexicographic.
/// Same wall-time events are ordered by logical counter;
/// ties within a node (impossible in practice) are broken by `node_id`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct HlcTimestamp {
    /// Wall-clock time in nanoseconds since UNIX epoch.
    ///
    /// Internally stored at millisecond granularity (bottom 6 decimal digits
    /// are always zero), but the nanosecond unit is preserved for API
    /// compatibility with other time sources.
    pub wall_ns: u64,
    /// Logical counter -- advances when wall time does not.
    pub logical: u32,
    /// The node (`ShardId.0`) that produced this timestamp.
    pub node_id: u16,
}

impl HlcTimestamp {
    /// Create a zero-valued HLC timestamp (minimum value).
    #[must_use]
    pub const fn zero() -> Self {
        Self {
            wall_ns: 0,
            logical: 0,
            node_id: 0,
        }
    }

    /// Convert `wall_ns` to milliseconds (truncating sub-ms precision).
    const fn wall_ms(&self) -> u64 {
        self.wall_ns / 1_000_000
    }
}

impl PartialOrd for HlcTimestamp {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for HlcTimestamp {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.wall_ns
            .cmp(&other.wall_ns)
            .then(self.logical.cmp(&other.logical))
            .then(self.node_id.cmp(&other.node_id))
    }
}

// ---------------------------------------------------------------------------
// Hlc
// ---------------------------------------------------------------------------

/// Per-node Hybrid Logical Clock.
///
/// Thread-safe: `now()` and `update()` use a single-word atomic CAS loop
/// to advance the clock without a mutex.  Multiple threads can call `now()`
/// concurrently; all returned timestamps are guaranteed unique within the
/// node.
///
/// # Examples
///
/// ```
/// use tidaldb::replication::crdt::hlc::{Hlc, HlcTimestamp};
///
/// let clock = Hlc::new(0);
/// let t1 = clock.now();
/// let t2 = clock.now();
/// assert!(t2 >= t1);
/// ```
pub struct Hlc {
    node_id: u16,
    /// Packed `(wall_ms << 16 | logical)`.  A single atomic word ensures
    /// that both components are advanced atomically via `compare_exchange`.
    packed: AtomicU64,
}

impl Hlc {
    /// Create a new clock for the given node.
    #[must_use]
    pub const fn new(node_id: u16) -> Self {
        Self {
            node_id,
            packed: AtomicU64::new(0),
        }
    }

    /// Create a clock for a [`ShardId`].
    #[must_use]
    pub const fn for_shard(shard: ShardId) -> Self {
        Self::new(shard.0)
    }

    /// Current wall-clock time in milliseconds since UNIX epoch.
    fn wall_ms_now() -> u64 {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as u64
    }

    /// Generate a new HLC timestamp for a local event.
    ///
    /// Advances the clock monotonically.  If the wall clock has not
    /// advanced since the last call, the logical counter increments
    /// (saturating at 65535 -- practically unreachable since it would
    /// require that many events within a single millisecond without
    /// the system clock advancing).
    pub fn now(&self) -> HlcTimestamp {
        let wall = Self::wall_ms_now();
        loop {
            // Acquire: ensures we see the latest packed word written by any
            // thread that previously completed a CAS in this loop.
            let cur = self.packed.load(Ordering::Acquire);
            let (cur_wall, cur_logical) = unpack(cur);

            let (new_wall, new_logical) = if wall > cur_wall {
                // Physical clock advanced -- reset the logical counter.
                (wall, 0u16)
            } else {
                // Physical clock did not advance (same or regressed).
                // Stay at the same wall_ms, bump logical.
                #[allow(clippy::cast_possible_truncation)]
                let next_logical = ((u64::from(cur_logical) + 1).min(LOGICAL_MAX)) as u16;
                (cur_wall, next_logical)
            };

            let new_packed = pack(new_wall, new_logical);

            // AcqRel on success: the write is visible to subsequent Acquire
            // loads.  Acquire on failure: re-read the latest value before
            // retry.
            if self
                .packed
                .compare_exchange(cur, new_packed, Ordering::AcqRel, Ordering::Acquire)
                .is_ok()
            {
                return HlcTimestamp {
                    wall_ns: new_wall.saturating_mul(1_000_000),
                    logical: u32::from(new_logical),
                    node_id: self.node_id,
                };
            }
            // CAS failed -- another thread advanced the clock.  Retry.
        }
    }

    /// Update the clock on receiving a remote HLC timestamp.
    ///
    /// Advances the local clock to be causally after `remote`, ensuring
    /// that any subsequent `now()` call produces a timestamp strictly
    /// greater than `remote`.
    ///
    /// Returns the new local timestamp (which is strictly > `remote`).
    pub fn update(&self, remote: HlcTimestamp) -> HlcTimestamp {
        let wall = Self::wall_ms_now();
        let remote_ms = remote.wall_ms();
        // Truncate remote.logical to u16 (safe: if remote.logical > u16::MAX
        // we saturate, which still advances past it).
        #[allow(clippy::cast_possible_truncation)]
        let remote_logical = (remote.logical.min(u32::from(u16::MAX))) as u16;

        loop {
            let cur = self.packed.load(Ordering::Acquire);
            let (cur_wall, cur_logical) = unpack(cur);

            // pt = max(wall, remote_ms, cur_wall)
            // This ensures we never go backwards from any known timestamp.
            let pt = wall.max(remote_ms).max(cur_wall);

            let new_logical = if pt == cur_wall && pt == remote_ms {
                // All three are equal -- must advance past both local
                // and remote logical counters.
                #[allow(clippy::cast_possible_truncation)]
                let l = ((u64::from(cur_logical.max(remote_logical)) + 1).min(LOGICAL_MAX)) as u16;
                l
            } else if pt == cur_wall {
                // Local wall is highest -- advance past local logical.
                #[allow(clippy::cast_possible_truncation)]
                let l = ((u64::from(cur_logical) + 1).min(LOGICAL_MAX)) as u16;
                l
            } else if pt == remote_ms {
                // Remote wall is highest -- advance past remote logical.
                #[allow(clippy::cast_possible_truncation)]
                let l = ((u64::from(remote_logical) + 1).min(LOGICAL_MAX)) as u16;
                l
            } else {
                // Physical clock is strictly ahead of both -- reset.
                0u16
            };

            let new_packed = pack(pt, new_logical);

            if self
                .packed
                .compare_exchange(cur, new_packed, Ordering::AcqRel, Ordering::Acquire)
                .is_ok()
            {
                return HlcTimestamp {
                    wall_ns: pt.saturating_mul(1_000_000),
                    logical: u32::from(new_logical),
                    node_id: self.node_id,
                };
            }
        }
    }
}

// Compile-time assertion: Hlc is Send + Sync (all fields are atomics or
// Copy primitives).
#[allow(dead_code)]
const fn _assert_hlc_send_sync() {
    const fn assert_send_sync<T: Send + Sync>() {}
    assert_send_sync::<Hlc>();
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    // -- Pack/unpack round-trip --

    #[test]
    fn pack_unpack_roundtrip() {
        let cases: Vec<(u64, u16)> = vec![
            (0, 0),
            (1, 0),
            (0, 1),
            (u64::MAX >> LOGICAL_BITS, u16::MAX),
            (1_740_000_000_000, 42), // ~2025 in ms
        ];
        for (wall_ms, logical) in cases {
            let packed = pack(wall_ms, logical);
            let (w, l) = unpack(packed);
            assert_eq!(w, wall_ms, "wall_ms mismatch for ({wall_ms}, {logical})");
            assert_eq!(l, logical, "logical mismatch for ({wall_ms}, {logical})");
        }
    }

    // -- HlcTimestamp ordering --

    #[test]
    fn hlc_timestamp_ordering_wall_ns() {
        let a = HlcTimestamp {
            wall_ns: 100,
            logical: 0,
            node_id: 0,
        };
        let b = HlcTimestamp {
            wall_ns: 200,
            logical: 0,
            node_id: 0,
        };
        assert!(a < b);
    }

    #[test]
    fn hlc_timestamp_ordering_logical() {
        let a = HlcTimestamp {
            wall_ns: 100,
            logical: 0,
            node_id: 0,
        };
        let b = HlcTimestamp {
            wall_ns: 100,
            logical: 1,
            node_id: 0,
        };
        assert!(a < b);
    }

    #[test]
    fn hlc_timestamp_ordering_node_id() {
        let a = HlcTimestamp {
            wall_ns: 100,
            logical: 0,
            node_id: 0,
        };
        let b = HlcTimestamp {
            wall_ns: 100,
            logical: 0,
            node_id: 1,
        };
        assert!(a < b);
    }

    // -- Hlc monotonicity --

    #[test]
    fn hlc_monotone_sequential() {
        let clock = Hlc::new(0);
        let mut prev = clock.now();
        for _ in 0..10_000 {
            let next = clock.now();
            assert!(next >= prev, "clock went backwards: {prev:?} > {next:?}");
            prev = next;
        }
    }

    // -- Hlc::update --

    #[test]
    fn hlc_update_advances_past_remote() {
        let clock = Hlc::new(0);
        // ~year 2096 in ms -- far enough in the future to exceed wall clock,
        // small enough that wall_ms * 1_000_000 does not overflow u64.
        let far_future_ms: u64 = 4_000_000_000_000;
        let remote = HlcTimestamp {
            wall_ns: far_future_ms * 1_000_000,
            logical: 99,
            node_id: 1,
        };
        let updated = clock.update(remote);
        assert!(
            updated > remote,
            "updated {updated:?} should be > remote {remote:?}"
        );
    }

    #[test]
    fn hlc_update_advances_past_local() {
        let clock = Hlc::new(0);
        // Advance the local clock first.
        let local = clock.now();
        // Remote is far in the past -- update should still advance past local.
        let remote = HlcTimestamp {
            wall_ns: 1_000_000, // 1 ms
            logical: 0,
            node_id: 1,
        };
        let updated = clock.update(remote);
        assert!(
            updated > local,
            "updated {updated:?} should be > local {local:?}"
        );
    }

    #[test]
    fn hlc_update_same_wall_as_remote_and_local() {
        let clock = Hlc::new(0);
        // ~year 2096 in ms -- far enough in the future to exceed wall clock.
        let far_future_ms: u64 = 4_000_000_000_000;
        let far_future_ns = far_future_ms * 1_000_000;
        let remote1 = HlcTimestamp {
            wall_ns: far_future_ns,
            logical: 10,
            node_id: 1,
        };
        let t1 = clock.update(remote1);
        assert!(t1 > remote1);

        // Now send another remote with the same wall_ns but lower logical.
        let remote2 = HlcTimestamp {
            wall_ns: far_future_ns,
            logical: 5,
            node_id: 2,
        };
        let t2 = clock.update(remote2);
        // t2 must be > both remote2 and t1.
        assert!(t2 > remote2);
        assert!(t2 > t1);
    }

    // -- Concurrent uniqueness --

    #[test]
    fn hlc_concurrent_uniqueness() {
        use std::sync::Arc;

        let clock = Arc::new(Hlc::new(0));
        let mut handles = vec![];
        for _ in 0..4 {
            let c = Arc::clone(&clock);
            handles.push(std::thread::spawn(move || {
                (0..250).map(|_| c.now()).collect::<Vec<_>>()
            }));
        }
        let mut all: Vec<HlcTimestamp> = handles
            .into_iter()
            .flat_map(|h| h.join().unwrap())
            .collect();
        all.sort();
        // All 1000 timestamps must be unique.
        for w in all.windows(2) {
            assert_ne!(w[0], w[1], "duplicate timestamp: {:?}", w[0]);
        }
    }

    // -- Zero --

    #[test]
    fn hlc_zero_is_minimum() {
        let zero = HlcTimestamp::zero();
        let clock = Hlc::new(0);
        let t = clock.now();
        assert!(t > zero);
    }

    // -- for_shard --

    #[test]
    fn hlc_for_shard() {
        let shard = ShardId(42);
        let clock = Hlc::for_shard(shard);
        let t = clock.now();
        assert_eq!(t.node_id, 42);
    }

    // -- wall_ns is at ms granularity --

    #[test]
    fn wall_ns_is_ms_granularity() {
        let clock = Hlc::new(0);
        let t = clock.now();
        assert_eq!(
            t.wall_ns % 1_000_000,
            0,
            "wall_ns should be a multiple of 1_000_000 (ms precision)"
        );
    }
}
