//! Per-domain CRDT merge strategies (ADR-027 / D19).
//!
//! Different entity domains in the operations runtime have different
//! merge semantics. The default [`HlcLastWriterWins`] preserves the
//! single-writer convention used before P2P sync was on the table; other
//! impls express:
//!
//! - [`OrSet`] for `BacklogItem` (add/remove of opaque items),
//! - [`TextMerge`] for ontology node descriptions (CRDT-friendly text),
//! - [`GSet`] for append-only domains like Q&A and bookmarks.

use std::collections::BTreeSet;

use ontoref_types::Hlc;
use serde::{Deserialize, Serialize};

/// Conflict-resolution strategy for a single entity domain.
pub trait CrdtMergeStrategy {
    /// The item type the strategy merges.
    type Item;

    /// Merge two values of the domain into one. MUST be commutative,
    /// associative, and idempotent — the standard CRDT properties.
    fn merge(&self, left: Self::Item, right: Self::Item) -> Self::Item;

    /// Witness bytes that explain the merge result for downstream
    /// verification. The default emits an empty witness (no
    /// verification surface); strategy impls SHOULD override this when
    /// the merge involves a non-trivial choice.
    fn merge_witness(&self, _left: &Self::Item, _right: &Self::Item) -> Vec<u8> {
        Vec::new()
    }
}

/// Default strategy: last-writer-wins ordered by hybrid logical clock,
/// breaking ties by the operation's payload bytes. Preserves the
/// single-writer convention of the pre-CRDT plan.
#[derive(Debug, Clone, Copy, Default)]
pub struct HlcLastWriterWins;

/// A timestamped opaque payload — the item type carried by
/// [`HlcLastWriterWins`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HlcStamped {
    /// Hybrid logical clock attached to the assertion.
    pub timestamp: Hlc,
    /// Opaque CBOR-encoded payload bytes.
    pub payload: Vec<u8>,
}

impl CrdtMergeStrategy for HlcLastWriterWins {
    type Item = HlcStamped;

    fn merge(&self, left: Self::Item, right: Self::Item) -> Self::Item {
        match left.timestamp.cmp(&right.timestamp) {
            std::cmp::Ordering::Less => right,
            std::cmp::Ordering::Greater => left,
            std::cmp::Ordering::Equal => {
                // Deterministic tie-break by payload bytes.
                if right.payload > left.payload {
                    right
                } else {
                    left
                }
            }
        }
    }

    fn merge_witness(&self, left: &Self::Item, right: &Self::Item) -> Vec<u8> {
        let winner = if self.merge(left.clone(), right.clone()).timestamp == right.timestamp {
            "right"
        } else {
            "left"
        };
        format!("hlc_lww:{winner}").into_bytes()
    }
}

/// Observed-Remove Set: add-wins semantics with explicit element tags.
/// Used for domains like `BacklogItem` where actors may add/remove
/// independently and the union with remove-bias is desired.
#[derive(Debug, Clone, Default)]
pub struct OrSet;

/// State of an OR-Set: tagged additions and explicit removals.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OrSetState<T: Ord + Clone> {
    /// (element, tag) pairs that have been added.
    pub adds: BTreeSet<(T, u64)>,
    /// Tags that have been removed; an element is in the set iff it has
    /// at least one add tag that is not in `removes`.
    pub removes: BTreeSet<u64>,
}

impl CrdtMergeStrategy for OrSet {
    type Item = OrSetState<String>;

    fn merge(&self, left: Self::Item, right: Self::Item) -> Self::Item {
        let mut adds = left.adds;
        adds.extend(right.adds);
        let mut removes = left.removes;
        removes.extend(right.removes);
        OrSetState { adds, removes }
    }
}

/// Text-merge strategy — for prose fields like ontology node descriptions.
/// Phase 1 keeps semantics simple: prefer the longer string (a
/// reasonable proxy for "more edited"); a future impl can plug in a
/// proper text-CRDT (Yjs, Automerge) without changing the trait.
#[derive(Debug, Clone, Copy, Default)]
pub struct TextMerge;

impl CrdtMergeStrategy for TextMerge {
    type Item = String;

    fn merge(&self, left: Self::Item, right: Self::Item) -> Self::Item {
        if right.len() > left.len() {
            right
        } else {
            left
        }
    }
}

/// Grow-only Set — for append-only domains like Q&A and bookmarks.
#[derive(Debug, Clone, Default)]
pub struct GSet;

impl CrdtMergeStrategy for GSet {
    type Item = BTreeSet<String>;

    fn merge(&self, left: Self::Item, right: Self::Item) -> Self::Item {
        let mut out = left;
        out.extend(right);
        out
    }
}

#[cfg(test)]
#[test]
fn hlc_lww_default() {
    let strategy = HlcLastWriterWins;
    let older = HlcStamped {
        timestamp: Hlc::new(1, 0),
        payload: b"older".to_vec(),
    };
    let newer = HlcStamped {
        timestamp: Hlc::new(2, 0),
        payload: b"newer".to_vec(),
    };

    // The newer timestamp wins regardless of argument order (commutative).
    let merged_a = strategy.merge(older.clone(), newer.clone());
    let merged_b = strategy.merge(newer.clone(), older.clone());
    assert_eq!(merged_a, newer);
    assert_eq!(merged_b, newer);

    // Idempotent: merging with self returns self.
    let merged_self = strategy.merge(newer.clone(), newer.clone());
    assert_eq!(merged_self, newer);

    // Tie-break by payload bytes when timestamps are equal.
    let tie_a = HlcStamped {
        timestamp: Hlc::new(5, 0),
        payload: b"aaa".to_vec(),
    };
    let tie_b = HlcStamped {
        timestamp: Hlc::new(5, 0),
        payload: b"bbb".to_vec(),
    };
    let merged_tie = strategy.merge(tie_a.clone(), tie_b.clone());
    assert_eq!(merged_tie, tie_b, "tie-break must pick the larger payload");

    // Merge witness records the winner.
    let witness = strategy.merge_witness(&older, &newer);
    assert_eq!(witness, b"hlc_lww:right");
}

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

    #[test]
    fn orset_add_remove_semantics() {
        let strategy = OrSet;
        let mut a: OrSetState<String> = OrSetState::default();
        a.adds.insert(("backlog-1".to_string(), 1));
        a.adds.insert(("backlog-2".to_string(), 2));

        let mut b: OrSetState<String> = OrSetState::default();
        b.adds.insert(("backlog-3".to_string(), 3));
        b.removes.insert(1);

        let merged = strategy.merge(a, b);
        assert_eq!(merged.adds.len(), 3, "all adds survive");
        assert_eq!(merged.removes.len(), 1, "removes are unioned");
    }

    #[test]
    fn text_merge_keeps_longer() {
        let strategy = TextMerge;
        let a = "short".to_string();
        let b = "much longer text".to_string();
        assert_eq!(strategy.merge(a.clone(), b.clone()), b);
        assert_eq!(strategy.merge(b.clone(), a), b);
    }

    #[test]
    fn gset_is_union() {
        let strategy = GSet;
        let mut a: BTreeSet<String> = BTreeSet::new();
        a.insert("x".into());
        a.insert("y".into());

        let mut b: BTreeSet<String> = BTreeSet::new();
        b.insert("y".into());
        b.insert("z".into());

        let merged = strategy.merge(a, b);
        assert_eq!(merged.len(), 3);
        assert!(merged.contains("x") && merged.contains("y") && merged.contains("z"));
    }
}
