//! Hybrid Logical Clock types for distributed causal ordering.
//!
//! HLCs combine physical time with node identity to provide:
//! - Causal ordering across distributed nodes
//! - Monotonic timestamps even with clock skew
//! - Total ordering when combined with node ID
//!
//! # Design
//!
//! This module provides a serializable wrapper around [`uhlc::Timestamp`] that
//! is compatible with rkyv zero-copy serialization. The wrapper stores:
//!
//! - `time_ntp64`: NTP64 encoded time (physical + logical in upper bits)
//! - `node_id`: 16-byte identifier for total ordering tiebreaker
//!
//! # Use Cases
//!
//! - **Supersession ordering**: Determine which supersession happened first
//!   across multiple nodes, even with clock skew
//! - **Conflict resolution**: Break ties in Last-Write-Wins (LWW) semantics
//! - **Replication**: Ensure causal consistency during CRDT merges
//!
//! # Example
//!
//! ```ignore
//! use stemedb_core::types::HlcTimestamp;
//!
//! // Create from uhlc::Timestamp
//! let hlc = HlcTimestamp::from_uhlc(&timestamp);
//!
//! // HLC timestamps are totally ordered
//! assert!(hlc1 < hlc2 || hlc1 > hlc2 || hlc1 == hlc2);
//! ```

use rkyv::{Archive, Deserialize, Serialize};
use std::cmp::Ordering;

/// A serializable Hybrid Logical Clock timestamp.
///
/// Provides causal ordering guarantees across distributed nodes. When comparing
/// two HLC timestamps:
///
/// 1. First compare `time_ntp64` (NTP64 encoded time with logical counter)
/// 2. If equal, compare `node_id` for total ordering
///
/// This ensures a total order even for concurrent events on different nodes.
///
/// # Serialization
///
/// Uses rkyv for zero-copy serialization, compatible with StemeDB's storage layer.
/// The archived form has identical layout for O(1) access.
#[derive(Archive, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[archive(check_bytes)]
pub struct HlcTimestamp {
    /// NTP64-encoded time with embedded logical counter.
    ///
    /// The upper bits contain the physical time (seconds since NTP epoch),
    /// and the lower bits may contain a logical counter for disambiguation
    /// of events at the same physical time.
    pub time_ntp64: u64,

    /// Node identifier for total ordering tiebreaker.
    ///
    /// When NTP64 time is equal (concurrent events on different nodes),
    /// the node ID provides a deterministic tiebreaker.
    /// Typically derived from a UUID or random bytes at node startup.
    pub node_id: [u8; 16],
}

impl HlcTimestamp {
    /// Creates a new HLC timestamp with the given components.
    ///
    /// # Arguments
    ///
    /// * `time_ntp64` - NTP64-encoded time value
    /// * `node_id` - 16-byte node identifier
    pub fn new(time_ntp64: u64, node_id: [u8; 16]) -> Self {
        Self { time_ntp64, node_id }
    }

    /// Creates an HLC timestamp from a `uhlc::Timestamp`.
    ///
    /// This is the primary constructor when using the `uhlc` crate for
    /// clock management.
    ///
    /// # Arguments
    ///
    /// * `ts` - A timestamp from the `uhlc` crate
    pub fn from_uhlc(ts: &uhlc::Timestamp) -> Self {
        Self { time_ntp64: ts.get_time().as_u64(), node_id: ts.get_id().to_le_bytes() }
    }

    /// Creates an HLC timestamp from the current time on a `uhlc::HLC` clock.
    ///
    /// This generates a new timestamp that is guaranteed to be greater than
    /// all previously generated timestamps from this clock.
    pub fn now(clock: &uhlc::HLC) -> Self {
        let ts = clock.new_timestamp();
        Self::from_uhlc(&ts)
    }

    /// Returns the time as milliseconds since Unix epoch (approximate).
    ///
    /// NTP64 encodes time differently than Unix timestamps. This method
    /// provides an approximate conversion for human-readable display.
    #[must_use]
    pub fn millis(&self) -> u64 {
        // NTP64 stores seconds in upper 32 bits, fractions in lower 32 bits
        // Convert to milliseconds: (ntp64 >> 32) * 1000 + ((ntp64 & 0xFFFFFFFF) * 1000 >> 32)
        let seconds = self.time_ntp64 >> 32;
        let fractions = self.time_ntp64 & 0xFFFF_FFFF;
        // NTP epoch is 1900-01-01, Unix epoch is 1970-01-01 (70 years = 2208988800 seconds)
        const NTP_UNIX_OFFSET: u64 = 2_208_988_800;
        let unix_seconds = seconds.saturating_sub(NTP_UNIX_OFFSET);
        let millis_from_fractions = (fractions * 1000) >> 32;
        unix_seconds * 1000 + millis_from_fractions
    }

    /// Returns the raw NTP64 time value for precise comparison.
    #[must_use]
    pub fn as_ntp64(&self) -> u64 {
        self.time_ntp64
    }

    /// Checks if this timestamp is causally before another.
    ///
    /// Note: This is based on the NTP64 time only, not the node ID.
    /// Two timestamps may be concurrent if they have the same time
    /// but different node IDs.
    #[must_use]
    pub fn is_before(&self, other: &Self) -> bool {
        self.time_ntp64 < other.time_ntp64
    }

    /// Returns true if this timestamp and another are concurrent.
    ///
    /// Concurrent means they have the same NTP64 time but different node IDs,
    /// indicating they were generated at the "same time" on different nodes
    /// without a causal relationship.
    #[must_use]
    pub fn is_concurrent_with(&self, other: &Self) -> bool {
        self.time_ntp64 == other.time_ntp64 && self.node_id != other.node_id
    }

    /// Converts this timestamp back to a `uhlc::Timestamp`.
    ///
    /// Useful for updating a clock with a received timestamp.
    #[must_use]
    pub fn to_uhlc(&self) -> Option<uhlc::Timestamp> {
        let id = uhlc::ID::try_from(&self.node_id[..]).ok()?;
        let time = uhlc::NTP64(self.time_ntp64);
        Some(uhlc::Timestamp::new(time, id))
    }
}

/// Total ordering for HLC timestamps.
///
/// Ordering is determined by:
/// 1. NTP64 time (includes physical + logical)
/// 2. Node ID (lexicographic)
impl Ord for HlcTimestamp {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.time_ntp64.cmp(&other.time_ntp64) {
            Ordering::Equal => self.node_id.cmp(&other.node_id),
            other => other,
        }
    }
}

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

/// Converts a `uhlc::ID` to a 16-byte array.
#[allow(dead_code)]
pub fn id_to_bytes(id: &uhlc::ID) -> [u8; 16] {
    id.to_le_bytes()
}

/// Creates a `uhlc::ID` from a 16-byte array.
///
/// Returns None if the bytes represent an invalid ID (all zeros).
pub fn bytes_to_id(bytes: [u8; 16]) -> Option<uhlc::ID> {
    uhlc::ID::try_from(&bytes[..]).ok()
}

/// Default skew threshold for clock drift detection (500ms in NTP64 units).
///
/// If the difference between local and remote physical time exceeds this
/// threshold, the clock should log a warning. This helps detect nodes
/// with significantly drifted clocks.
///
/// 500ms = 0.5 seconds. In NTP64, this is approximately 0x80000000 (half of
/// the fractional second range in the lower 32 bits).
pub const SKEW_THRESHOLD_MS: u64 = 500;

/// Checks if two HLC timestamps indicate clock skew beyond the threshold.
///
/// Returns `Some(skew_ms)` if the physical time difference exceeds
/// `SKEW_THRESHOLD_MS`, otherwise `None`.
///
/// # Use Case
///
/// When merging CRDT state from a remote node, check for clock skew
/// and log a warning if detected. This helps operators identify nodes
/// that need NTP synchronization.
pub fn detect_clock_skew(local: &HlcTimestamp, remote: &HlcTimestamp) -> Option<u64> {
    let local_ms = local.millis();
    let remote_ms = remote.millis();
    let diff = local_ms.abs_diff(remote_ms);

    if diff > SKEW_THRESHOLD_MS {
        Some(diff)
    } else {
        None
    }
}

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

    #[test]
    fn test_hlc_ordering_time() {
        let node_id = [1u8; 16];
        let t1 = HlcTimestamp::new(1000, node_id);
        let t2 = HlcTimestamp::new(2000, node_id);

        assert!(t1 < t2);
        assert!(t2 > t1);
    }

    #[test]
    fn test_hlc_ordering_node_id() {
        let t1 = HlcTimestamp::new(1000, [1u8; 16]);
        let t2 = HlcTimestamp::new(1000, [2u8; 16]);

        assert!(t1 < t2);
        assert!(t2 > t1);
    }

    #[test]
    fn test_hlc_equality() {
        let t1 = HlcTimestamp::new(1000, [1u8; 16]);
        let t2 = HlcTimestamp::new(1000, [1u8; 16]);

        assert_eq!(t1, t2);
    }

    #[test]
    fn test_is_before() {
        let node_id = [1u8; 16];
        let t1 = HlcTimestamp::new(1000, node_id);
        let t2 = HlcTimestamp::new(2000, node_id);
        let t3 = HlcTimestamp::new(3000, node_id);

        assert!(t1.is_before(&t2));
        assert!(t1.is_before(&t3));
        assert!(t2.is_before(&t3));
        assert!(!t2.is_before(&t1));
    }

    #[test]
    fn test_is_concurrent() {
        let t1 = HlcTimestamp::new(1000, [1u8; 16]);
        let t2 = HlcTimestamp::new(1000, [2u8; 16]);
        let t3 = HlcTimestamp::new(2000, [1u8; 16]);

        assert!(t1.is_concurrent_with(&t2));
        assert!(!t1.is_concurrent_with(&t3));
    }

    #[test]
    fn test_detect_clock_skew() {
        // Test with realistic NTP64 values (after NTP epoch offset)
        // NTP epoch offset: 2208988800 seconds from 1900 to 1970

        // 1 second in NTP64 = 1 << 32 (upper 32 bits are seconds)
        let ntp_seconds = |s: u64| s << 32;

        // Use a time after the NTP-Unix offset so millis() returns positive values
        // NTP_UNIX_OFFSET = 2_208_988_800, so use a time after that
        const BASE_NTP_SECONDS: u64 = 2_208_988_800 + 1000; // 1000 seconds after Unix epoch

        let local = HlcTimestamp::new(ntp_seconds(BASE_NTP_SECONDS), [1u8; 16]);
        let remote_ok = HlcTimestamp::new(ntp_seconds(BASE_NTP_SECONDS), [2u8; 16]);

        // No skew - same time
        assert!(detect_clock_skew(&local, &remote_ok).is_none());

        // Create a timestamp 1 second ahead (1000ms > 500ms threshold)
        let remote_skew = HlcTimestamp::new(ntp_seconds(BASE_NTP_SECONDS + 1), [2u8; 16]);

        // This should detect skew (1000ms > 500ms threshold)
        let skew = detect_clock_skew(&local, &remote_skew);
        assert!(skew.is_some(), "Expected skew detection, got None");

        // The skew should be ~1000ms (1 second)
        assert_eq!(skew, Some(1000));

        // Test with 400ms difference (below threshold)
        // 400ms = 0.4 * 2^32 ≈ 1717986918 in NTP64 fractional part
        let remote_under_threshold =
            HlcTimestamp::new(ntp_seconds(BASE_NTP_SECONDS) + 1717986918, [2u8; 16]);
        assert!(detect_clock_skew(&local, &remote_under_threshold).is_none());
    }

    #[test]
    fn test_from_uhlc_roundtrip() {
        // Create a uhlc clock and generate a timestamp
        let clock = uhlc::HLCBuilder::new().build();
        let ts = clock.new_timestamp();

        // Convert to our format
        let hlc = HlcTimestamp::from_uhlc(&ts);

        // Convert back
        let recovered = hlc.to_uhlc().expect("should convert back");

        // Should be equal
        assert_eq!(ts, recovered);
    }

    #[test]
    fn test_hlc_now() {
        let clock = uhlc::HLCBuilder::new().build();

        let t1 = HlcTimestamp::now(&clock);
        let t2 = HlcTimestamp::now(&clock);

        // t2 should be >= t1 (monotonic)
        assert!(t2 >= t1);

        // Both should have the same node ID
        assert_eq!(t1.node_id, t2.node_id);
    }

    #[test]
    fn test_serialization_roundtrip() {
        use crate::serde::{deserialize, serialize};

        let ts = HlcTimestamp::new(
            12345678901234,
            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
        );

        let bytes = serialize(&ts).expect("serialize");
        let deserialized: HlcTimestamp = deserialize(&bytes).expect("deserialize");

        assert_eq!(ts, deserialized);
    }

    #[test]
    fn test_default() {
        let ts = HlcTimestamp::default();
        assert_eq!(ts.time_ntp64, 0);
        assert_eq!(ts.node_id, [0u8; 16]);
    }
}
