//! CRDT document types, read-side view types, and CRDT-state events.

#![allow(unused_imports, dead_code)]
use bft_json_crdt::json_crdt::*;
use bft_json_crdt::list_crdt::ListCrdt;
use bft_json_crdt::lww_crdt::LwwRegisterCrdt;
use std::sync::OnceLock;
use tokio::sync::broadcast;

/// An event emitted when a pipeline item's stage changes in the CRDT document.
#[derive(Clone, Debug)]
pub struct CrdtEvent {
    /// Work item ID (e.g. `"42_story_my_feature"`).
    pub story_id: String,
    /// The stage the item was in before this transition, or `None` for new items.
    pub from_stage: Option<String>,
    /// The stage the item is now in.
    pub to_stage: String,
    /// Human-readable story name from the CRDT document.
    pub name: Option<String>,
}

/// Subscribe to CRDT state transition events.
///
/// Returns `None` if the CRDT layer has not been initialised yet.
pub fn subscribe() -> Option<broadcast::Receiver<CrdtEvent>> {
    CRDT_EVENT_TX.get().map(|tx| tx.subscribe())
}

static CRDT_EVENT_TX: OnceLock<broadcast::Sender<CrdtEvent>> = OnceLock::new();

// ── CRDT document types ──────────────────────────────────────────────

#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct PipelineDoc {
    pub items: ListCrdt<PipelineItemCrdt>,
    pub nodes: ListCrdt<NodePresenceCrdt>,
}

#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct PipelineItemCrdt {
    pub story_id: LwwRegisterCrdt<String>,
    pub stage: LwwRegisterCrdt<String>,
    pub name: LwwRegisterCrdt<String>,
    pub agent: LwwRegisterCrdt<String>,
    pub retry_count: LwwRegisterCrdt<f64>,
    pub blocked: LwwRegisterCrdt<bool>,
    pub depends_on: LwwRegisterCrdt<String>,
    /// Node ID (hex-encoded Ed25519 pubkey) of the node that claimed this item.
    /// Used for distributed work claiming — the LWW register resolves conflicts
    /// deterministically so all nodes converge on the same claimer.
    pub claimed_by: LwwRegisterCrdt<String>,
    /// Unix timestamp (seconds) when the claim was written.
    /// Used for timeout-based reclaim: if a node crashes, other nodes can
    /// reclaim the item after the timeout expires.
    pub claimed_at: LwwRegisterCrdt<f64>,
    /// Unix timestamp (seconds) when the item was merged to master.
    /// Written once when the item transitions to `5_done`.  Used by the
    /// sweep loop to determine when to promote to `6_archived`.
    pub merged_at: LwwRegisterCrdt<f64>,
}

/// CRDT node that holds a single peer's presence entry.
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct NodePresenceCrdt {
    /// Hex-encoded Ed25519 public key — stable identity across restarts.
    pub node_id: LwwRegisterCrdt<String>,
    /// WebSocket URL this peer advertises, e.g. `ws://192.168.1.10:3001/crdt-sync`.
    pub address: LwwRegisterCrdt<String>,
    /// Unix timestamp (seconds) of the last heartbeat written by this node.
    pub last_seen: LwwRegisterCrdt<f64>,
    /// `false` once a stale-detection pass has tombstoned this node.
    pub alive: LwwRegisterCrdt<bool>,
}

// ── Read-side view types ─────────────────────────────────────────────

/// A snapshot of a single pipeline item derived from the CRDT document.
#[derive(Clone, Debug)]
pub struct PipelineItemView {
    pub story_id: String,
    pub stage: String,
    pub name: Option<String>,
    pub agent: Option<String>,
    pub retry_count: Option<i64>,
    pub blocked: Option<bool>,
    pub depends_on: Option<Vec<u32>>,
    /// Node ID of the node that claimed this item (hex-encoded Ed25519 pubkey).
    pub claimed_by: Option<String>,
    /// Unix timestamp when the item was claimed.
    pub claimed_at: Option<f64>,
    /// Unix timestamp (seconds) when the item was merged to master.
    /// `None` for items that were never in `5_done` or for legacy items.
    pub merged_at: Option<f64>,
}

/// A snapshot of a single node presence entry derived from the CRDT document.
#[derive(Clone, Debug)]
pub struct NodePresenceView {
    pub node_id: String,
    pub address: String,
    /// Unix timestamp (seconds).
    pub last_seen: f64,
    pub alive: bool,
}

#[cfg(test)]
mod tests {
    use super::super::state::emit_event;
    use super::super::state::init_for_test;
    use super::super::write::write_item;
    use super::*;
    use bft_json_crdt::json_crdt::OpState;
    use bft_json_crdt::keypair::make_keypair;
    use bft_json_crdt::op::ROOT_ID;
    use serde_json::json;

    #[test]
    fn crdt_doc_insert_and_view() {
        let kp = make_keypair();
        let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);

        let item_json: JsonValue = json!({
            "story_id": "10_story_test",
            "stage": "2_current",
            "name": "Test Story",
            "agent": "coder-opus",
            "retry_count": 0.0,
            "blocked": false,
            "depends_on": "",
            "claimed_by": "",
            "claimed_at": 0.0,
        })
        .into();

        let op = crdt.doc.items.insert(ROOT_ID, item_json).sign(&kp);
        assert_eq!(crdt.apply(op), OpState::Ok);

        let view = crdt.doc.items.view();
        assert_eq!(view.len(), 1);

        let item = &crdt.doc.items[0];
        assert_eq!(
            item.story_id.view(),
            JsonValue::String("10_story_test".to_string())
        );
        assert_eq!(
            item.stage.view(),
            JsonValue::String("2_current".to_string())
        );
    }

    #[test]
    fn crdt_doc_update_stage() {
        let kp = make_keypair();
        let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);

        let item_json: JsonValue = json!({
            "story_id": "20_story_move",
            "stage": "1_backlog",
            "name": "Move Me",
            "agent": "",
            "retry_count": 0.0,
            "blocked": false,
            "depends_on": "",
            "claimed_by": "",
            "claimed_at": 0.0,
        })
        .into();

        let insert_op = crdt.doc.items.insert(ROOT_ID, item_json).sign(&kp);
        crdt.apply(insert_op);

        // Update stage
        let stage_op = crdt.doc.items[0]
            .stage
            .set("2_current".to_string())
            .sign(&kp);
        crdt.apply(stage_op);

        assert_eq!(
            crdt.doc.items[0].stage.view(),
            JsonValue::String("2_current".to_string())
        );
    }

    #[test]
    fn crdt_event_has_expected_fields() {
        let evt = CrdtEvent {
            story_id: "42_story_foo".to_string(),
            from_stage: Some("1_backlog".to_string()),
            to_stage: "2_current".to_string(),
            name: Some("Foo Feature".to_string()),
        };
        assert_eq!(evt.story_id, "42_story_foo");
        assert_eq!(evt.from_stage.as_deref(), Some("1_backlog"));
        assert_eq!(evt.to_stage, "2_current");
        assert_eq!(evt.name.as_deref(), Some("Foo Feature"));
    }

    #[test]
    fn crdt_event_clone_preserves_data() {
        let evt = CrdtEvent {
            story_id: "10_story_bar".to_string(),
            from_stage: None,
            to_stage: "1_backlog".to_string(),
            name: None,
        };
        let cloned = evt.clone();
        assert_eq!(cloned.story_id, "10_story_bar");
        assert!(cloned.from_stage.is_none());
        assert!(cloned.name.is_none());
    }

    #[test]
    fn emit_event_is_noop_when_channel_not_initialised() {
        // Before CRDT_EVENT_TX is set, emit_event should not panic.
        // This test verifies the guard clause works. In test binaries the
        // OnceLock may already be set by another test, so we just verify
        // the function doesn't panic regardless.
        emit_event(CrdtEvent {
            story_id: "99_story_noop".to_string(),
            from_stage: None,
            to_stage: "1_backlog".to_string(),
            name: None,
        });
    }

    #[test]
    fn crdt_event_broadcast_channel_round_trip() {
        let (tx, mut rx) = broadcast::channel::<CrdtEvent>(16);
        let evt = CrdtEvent {
            story_id: "70_story_broadcast".to_string(),
            from_stage: Some("1_backlog".to_string()),
            to_stage: "2_current".to_string(),
            name: Some("Broadcast Test".to_string()),
        };
        tx.send(evt).unwrap();

        let received = rx.try_recv().unwrap();
        assert_eq!(received.story_id, "70_story_broadcast");
        assert_eq!(received.from_stage.as_deref(), Some("1_backlog"));
        assert_eq!(received.to_stage, "2_current");
        assert_eq!(received.name.as_deref(), Some("Broadcast Test"));
    }
}
