use std::collections::HashSet;
use std::time::{Duration, Instant};

use super::format::{EventRecord, event_content_hash};

/// Double-buffered deduplication window.
///
/// Maintains two `HashSet<u128>` buffers that rotate every `window` duration.
/// An event is considered a duplicate if its truncated BLAKE3 hash (first 128 bits)
/// exists in either the current or previous buffer.
///
/// Zero false positives: unique events are never incorrectly classified as duplicates.
/// The 128-bit hash space gives a collision probability of ~2.7e-26 at 3M entries,
/// which is effectively zero.
pub struct DedupWindow {
    current: HashSet<u128>,
    previous: HashSet<u128>,
    rotation_time: Instant,
    window: Duration,
}

impl DedupWindow {
    /// Create a new dedup window with the given rotation interval.
    #[must_use]
    pub fn new(window: Duration) -> Self {
        Self {
            current: HashSet::new(),
            previous: HashSet::new(),
            rotation_time: Instant::now(),
            window,
        }
    }

    /// Check if the event is a duplicate. If not, insert it into the current window.
    ///
    /// Returns `true` if the event has been seen before (within the dedup window).
    pub fn is_duplicate(&mut self, event: &EventRecord) -> bool {
        self.maybe_rotate();
        let hash = event_content_hash(event);
        if self.current.contains(&hash) || self.previous.contains(&hash) {
            return true;
        }
        self.current.insert(hash);
        false
    }

    /// Populate the dedup window from replayed events during recovery.
    ///
    /// This inserts all provided events into the current window so that
    /// duplicates arriving after recovery are correctly detected.
    pub fn populate_from_events<I: IntoIterator<Item = EventRecord>>(&mut self, events: I) {
        for event in events {
            let hash = event_content_hash(&event);
            self.current.insert(hash);
        }
    }

    /// Rotate buffers if the window duration has elapsed.
    fn maybe_rotate(&mut self) {
        if self.rotation_time.elapsed() >= self.window {
            std::mem::swap(&mut self.current, &mut self.previous);
            self.current.clear();
            self.rotation_time = Instant::now();
        }
    }
}

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

    fn make_event(id: u64) -> EventRecord {
        EventRecord {
            entity_id: id,
            signal_type: 1,
            weight: 1.0,
            timestamp_nanos: 1_000_000_000,
        }
    }

    #[test]
    fn unique_event_not_duplicate() {
        let mut dedup = DedupWindow::new(Duration::from_secs(30));
        assert!(!dedup.is_duplicate(&make_event(1)));
    }

    #[test]
    fn duplicate_detected() {
        let mut dedup = DedupWindow::new(Duration::from_secs(30));
        let event = make_event(1);
        assert!(!dedup.is_duplicate(&event));
        assert!(dedup.is_duplicate(&event));
    }

    #[test]
    fn different_events_not_duplicate() {
        let mut dedup = DedupWindow::new(Duration::from_secs(30));
        assert!(!dedup.is_duplicate(&make_event(1)));
        assert!(!dedup.is_duplicate(&make_event(2)));
    }

    #[test]
    fn rotation_clears_old_window() {
        // Use a zero-duration window to force immediate rotation.
        let mut dedup = DedupWindow::new(Duration::ZERO);
        let event = make_event(1);

        assert!(!dedup.is_duplicate(&event));
        // After inserting, the event is in `current`. Sleeping is not needed
        // because Duration::ZERO means rotation triggers on the next call.
        // The event is now in `previous`, still detectable.
        assert!(dedup.is_duplicate(&event));
        // One more rotation moves previous->cleared, current->previous.
        // The event was in previous (from the first swap), now it goes away
        // after a second rotation. We need to trigger two rotations.
        // Force a second rotation by calling is_duplicate again --
        // but actually, with ZERO window, every call to maybe_rotate rotates.
        // After the first is_duplicate(event) call that returned true:
        //   - maybe_rotate was called, swapping current<->previous, clearing new current
        //   - event found in previous (where it was inserted), returned true
        // On the third call:
        //   - maybe_rotate swaps again: previous (which had our hash) goes to current,
        //     but then current is cleared. Actually no -- swap then clear current.
        //   Wait. Let me trace through:
        //
        // State after new(): current={}, previous={}, time=T0
        // Call 1: is_duplicate(event)
        //   maybe_rotate: elapsed >= 0 -> swap. current(now empty)=previous(was empty).
        //                 previous(was current)=current(was empty). clear current.
        //                 State: current={}, previous={}
        //   Not found. Insert into current. State: current={H}, previous={}
        //   Returns false.
        // Call 2: is_duplicate(event)
        //   maybe_rotate: elapsed >= 0 -> swap. current(was {H}) goes to previous.
        //                 previous(was {}) goes to current. clear current.
        //                 State: current={}, previous={H}
        //   Found in previous. Returns true.
        // Call 3: is_duplicate(event)
        //   maybe_rotate: swap. current(was {}) -> previous. previous(was {H}) -> current.
        //                 clear current. State: current={}, previous={}
        //   Not found. Insert into current. State: current={H}, previous={}
        //   Returns false.

        // So after two rotations, the event is gone from both buffers.
        assert!(!dedup.is_duplicate(&event));
    }

    #[test]
    fn populate_from_events_enables_dedup() {
        let mut dedup = DedupWindow::new(Duration::from_secs(30));
        let events = vec![make_event(10), make_event(20)];
        dedup.populate_from_events(events);

        assert!(dedup.is_duplicate(&make_event(10)));
        assert!(dedup.is_duplicate(&make_event(20)));
        assert!(!dedup.is_duplicate(&make_event(30)));
    }

    #[test]
    fn no_false_positives_many_unique_events() {
        let mut dedup = DedupWindow::new(Duration::from_secs(60));
        let count = 100_000;
        for i in 0..count {
            let event = EventRecord {
                entity_id: i,
                signal_type: (i % 256) as u8,
                weight: i as f32,
                timestamp_nanos: i * 1_000_000,
            };
            assert!(!dedup.is_duplicate(&event), "false positive at event {i}");
        }
    }

    mod proptests {
        use super::*;
        use proptest::prelude::*;

        fn arb_event() -> impl Strategy<Value = EventRecord> {
            (any::<u64>(), any::<u8>(), any::<f32>(), any::<u64>()).prop_map(
                |(entity_id, signal_type, weight, timestamp_nanos)| EventRecord {
                    entity_id,
                    signal_type,
                    weight,
                    timestamp_nanos,
                },
            )
        }

        proptest! {
            #[test]
            fn unique_events_never_dropped(
                events in proptest::collection::vec(arb_event(), 1..=1000)
            ) {
                // Generate events with unique (entity_id, signal_type, timestamp_nanos, weight)
                // combinations by using index-derived values.
                let mut dedup = DedupWindow::new(Duration::from_secs(60));
                let unique_events: Vec<EventRecord> = events
                    .iter()
                    .enumerate()
                    .map(|(i, e)| EventRecord {
                        entity_id: i as u64,
                        signal_type: e.signal_type,
                        weight: e.weight,
                        timestamp_nanos: i as u64 * 1_000_000 + u64::from(e.signal_type),
                    })
                    .collect();

                for (i, event) in unique_events.iter().enumerate() {
                    prop_assert!(
                        !dedup.is_duplicate(event),
                        "false positive at index {i}"
                    );
                }
            }
        }
    }
}
