//! Positive-Negative Counter CRDT.
//!
//! A PN-Counter maintains per-node P (increment) and N (decrement) totals.
//! The global value = sum(P) - sum(N). Merge takes the per-node maximum of
//! each component, making it commutative, associative, and idempotent.

use std::collections::HashMap;

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

/// Positive-Negative Counter CRDT.
///
/// Each node ([`ShardId`]) maintains its own P (increment) and N (decrement)
/// totals. The global value = `sum(P) - sum(N)`. Merge takes the per-node
/// max of each component -- safe because values only ever increase within
/// a node.
///
/// # Properties
///
/// - **Commutative:** `merge(A, B) == merge(B, A)`
/// - **Associative:** `merge(A, merge(B, C)) == merge(merge(A, B), C)`
/// - **Idempotent:** `merge(A, A) == A`
///
/// # Examples
///
/// ```
/// use tidaldb::replication::crdt::PNCounter;
/// use tidaldb::replication::ShardId;
///
/// let mut a = PNCounter::new();
/// let mut b = PNCounter::new();
/// a.increment(ShardId(0), 10);
/// b.increment(ShardId(1), 5);
/// a.merge(&b);
/// assert_eq!(a.value(), 15);
/// ```
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PNCounter {
    positive: HashMap<ShardId, u64>,
    negative: HashMap<ShardId, u64>,
}

impl PNCounter {
    /// Create a new empty counter.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Increment the counter for `node` by `amount`.
    pub fn increment(&mut self, node: ShardId, amount: u64) {
        *self.positive.entry(node).or_default() += amount;
    }

    /// Decrement the counter for `node` by `amount`.
    pub fn decrement(&mut self, node: ShardId, amount: u64) {
        *self.negative.entry(node).or_default() += amount;
    }

    /// Merge another counter into this one.
    ///
    /// Takes the per-node maximum of both P and N components.
    /// Safe because each node's contribution only grows.
    pub fn merge(&mut self, other: &Self) {
        for (&node, &val) in &other.positive {
            let entry = self.positive.entry(node).or_default();
            *entry = (*entry).max(val);
        }
        for (&node, &val) in &other.negative {
            let entry = self.negative.entry(node).or_default();
            *entry = (*entry).max(val);
        }
    }

    /// Returns the current value: `sum(P) - sum(N)`, saturating at 0.
    #[must_use]
    pub fn value(&self) -> u64 {
        let p: u64 = self.positive.values().sum();
        let n: u64 = self.negative.values().sum();
        p.saturating_sub(n)
    }

    /// Total positive contributions across all nodes.
    #[must_use]
    pub fn total_positive(&self) -> u64 {
        self.positive.values().sum()
    }

    /// Total negative contributions across all nodes.
    #[must_use]
    pub fn total_negative(&self) -> u64 {
        self.negative.values().sum()
    }
}

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

    #[test]
    fn increment_and_value() {
        let mut c = PNCounter::new();
        c.increment(ShardId(0), 10);
        c.increment(ShardId(1), 5);
        assert_eq!(c.value(), 15);
    }

    #[test]
    fn decrement_saturates_at_zero() {
        let mut c = PNCounter::new();
        c.increment(ShardId(0), 5);
        c.decrement(ShardId(0), 10);
        assert_eq!(c.value(), 0);
    }

    #[test]
    fn merge_commutative() {
        let mut a = PNCounter::new();
        a.increment(ShardId(0), 10);
        let mut b = PNCounter::new();
        b.increment(ShardId(1), 5);

        let mut ab = a.clone();
        ab.merge(&b);
        let mut ba = b.clone();
        ba.merge(&a);
        assert_eq!(ab.value(), ba.value());
        assert_eq!(ab, ba);
    }

    #[test]
    fn merge_idempotent() {
        let mut c = PNCounter::new();
        c.increment(ShardId(0), 42);
        let snapshot = c.clone();
        c.merge(&snapshot);
        assert_eq!(c.value(), snapshot.value());
        assert_eq!(c, snapshot);
    }

    #[test]
    fn merge_associative() {
        let mut a = PNCounter::new();
        a.increment(ShardId(0), 10);
        let mut b = PNCounter::new();
        b.increment(ShardId(1), 20);
        let mut c = PNCounter::new();
        c.increment(ShardId(2), 30);

        // (A merge B) merge C
        let mut ab_c = a.clone();
        ab_c.merge(&b);
        ab_c.merge(&c);

        // A merge (B merge C)
        let mut a_bc = a.clone();
        let mut bc = b.clone();
        bc.merge(&c);
        a_bc.merge(&bc);

        assert_eq!(ab_c.value(), a_bc.value());
        assert_eq!(ab_c, a_bc);
    }

    #[test]
    fn no_double_count_disjoint_nodes() {
        let mut a = PNCounter::new();
        a.increment(ShardId(0), 100);
        let mut b = PNCounter::new();
        b.increment(ShardId(1), 200);
        a.merge(&b);
        assert_eq!(a.value(), 300);
    }

    #[test]
    fn merge_same_node_takes_max() {
        let mut a = PNCounter::new();
        a.increment(ShardId(0), 50);
        let mut b = PNCounter::new();
        b.increment(ShardId(0), 30);

        a.merge(&b);
        // Node 0's positive should be max(50, 30) = 50
        assert_eq!(a.value(), 50);
    }

    #[test]
    fn total_positive_and_negative() {
        let mut c = PNCounter::new();
        c.increment(ShardId(0), 10);
        c.increment(ShardId(1), 5);
        c.decrement(ShardId(0), 3);
        assert_eq!(c.total_positive(), 15);
        assert_eq!(c.total_negative(), 3);
        assert_eq!(c.value(), 12);
    }

    #[test]
    fn empty_counter_is_zero() {
        let c = PNCounter::new();
        assert_eq!(c.value(), 0);
        assert_eq!(c.total_positive(), 0);
        assert_eq!(c.total_negative(), 0);
    }

    #[test]
    fn merge_with_decrements() {
        let mut a = PNCounter::new();
        a.increment(ShardId(0), 100);
        a.decrement(ShardId(0), 20);

        let mut b = PNCounter::new();
        b.increment(ShardId(1), 50);
        b.decrement(ShardId(1), 10);

        a.merge(&b);
        // P = max(100, -) + max(-, 50) = 150
        // N = max(20, -) + max(-, 10) = 30
        assert_eq!(a.value(), 120);
    }

    #[test]
    fn serde_roundtrip() {
        let mut c = PNCounter::new();
        c.increment(ShardId(0), 42);
        c.decrement(ShardId(1), 7);

        let json = serde_json::to_string(&c).expect("serialize");
        let deserialized: PNCounter = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(c, deserialized);
    }
}
