//! CRDT scaffolding for collaborative editing.
//!
//! This crate defines traits and types for integrating a CRDT backend
//! (e.g. Yjs, Automerge, Loro) with the devup-editor engine.
//!
//! ## Status: SCAFFOLDING ONLY
//!
//! No actual CRDT implementation yet. Traits and types are provided so
//! downstream crates can start building against a stable interface.

use devup_editor_core::{Document, Operation};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CrdtError {
    #[error("CRDT sync error: {0}")]
    Sync(String),
}

/// Globally unique client identifier for a collaborative session.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ClientId(pub u64);

/// A logical timestamp (e.g. Lamport clock).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Timestamp(pub u64);

/// A single CRDT update that can be exchanged with peers.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Update {
    pub client: ClientId,
    pub timestamp: Timestamp,
    /// Opaque payload — the format is CRDT-implementation-specific.
    pub payload: Vec<u8>,
}

/// A CRDT backend that can track document changes and produce/apply updates.
pub trait CrdtBackend {
    /// Initialize the CRDT with a starting document snapshot.
    fn from_document(doc: &Document, client: ClientId) -> Self
    where
        Self: Sized;

    /// Apply a local `Operation` (from the devup-editor engine) to the CRDT
    /// and return any update bytes that should be broadcast to peers.
    ///
    /// # Errors
    /// Returns [`CrdtError`] if the operation cannot be represented in the
    /// underlying CRDT state.
    fn local_op(&mut self, op: &Operation) -> Result<Update, CrdtError>;

    /// Apply a remote update (received from a peer) and return the list of
    /// Operations that should be applied to the local Document to sync state.
    ///
    /// # Errors
    /// Returns [`CrdtError`] if the update is invalid or cannot be merged into
    /// the local CRDT state.
    fn remote_update(&mut self, update: &Update) -> Result<Vec<Operation>, CrdtError>;

    /// Get the current document snapshot as seen by the CRDT.
    fn snapshot(&self) -> Document;

    /// Produce a state vector that peers can use to compute missing updates.
    fn state_vector(&self) -> Vec<u8>;

    /// Given a remote state vector, return the updates needed to bring that
    /// peer up to date.
    fn diff(&self, remote_state_vector: &[u8]) -> Vec<Update>;
}

/// In-memory no-op backend for testing. Does NOT implement actual CRDT semantics.
pub struct NoOpBackend {
    doc: Document,
    client: ClientId,
    clock: u64,
}

impl CrdtBackend for NoOpBackend {
    fn from_document(doc: &Document, client: ClientId) -> Self {
        Self {
            doc: doc.clone(),
            client,
            clock: 0,
        }
    }

    fn local_op(&mut self, _op: &Operation) -> Result<Update, CrdtError> {
        self.clock += 1;
        Ok(Update {
            client: self.client,
            timestamp: Timestamp(self.clock),
            payload: vec![],
        })
    }

    fn remote_update(&mut self, _update: &Update) -> Result<Vec<Operation>, CrdtError> {
        Ok(vec![])
    }

    fn snapshot(&self) -> Document {
        self.doc.clone()
    }

    fn state_vector(&self) -> Vec<u8> {
        self.clock.to_le_bytes().to_vec()
    }

    fn diff(&self, _remote_state_vector: &[u8]) -> Vec<Update> {
        vec![]
    }
}

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

    #[test]
    fn noop_backend_smoke() {
        let doc = Document::new();
        let mut backend = NoOpBackend::from_document(&doc, ClientId(1));
        let update = backend
            .local_op(&Operation::SetSelection { selection: None })
            .unwrap();
        assert_eq!(update.client, ClientId(1));
        assert_eq!(update.timestamp, Timestamp(1));
    }

    #[test]
    fn noop_snapshot_matches_input() {
        let doc = Document::new();
        let backend = NoOpBackend::from_document(&doc, ClientId(42));
        let snap = backend.snapshot();
        assert_eq!(snap.root_block_count(), doc.root_block_count());
    }
}
