//! 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>,
    pub tokens: ListCrdt<TokenUsageCrdt>,
    pub merge_jobs: ListCrdt<MergeJobCrdt>,
    pub active_agents: ListCrdt<ActiveAgentCrdt>,
    pub test_jobs: ListCrdt<TestJobCrdt>,
    pub agent_throttle: ListCrdt<AgentThrottleCrdt>,
}

#[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>,
    /// Human-readable display name for this node, e.g. `"builder-1"`.
    pub label: LwwRegisterCrdt<String>,
    /// Project slug this node is assigned to, or empty string if unassigned.
    pub assigned_project: LwwRegisterCrdt<String>,
    /// Unix timestamp in **milliseconds** of the last heartbeat (higher precision than `last_seen`).
    pub last_seen_ms: LwwRegisterCrdt<f64>,
}

// ── 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, serde::Serialize)]
pub struct NodePresenceView {
    pub node_id: String,
    pub address: String,
    /// Unix timestamp (seconds).
    pub last_seen: f64,
    pub alive: bool,
    /// Human-readable display name; `None` if not yet set.
    pub label: Option<String>,
    /// Project slug this node is assigned to; `None` if unassigned.
    pub assigned_project: Option<String>,
    /// Unix timestamp in milliseconds; `None` if not yet set.
    pub last_seen_ms: Option<f64>,
}

// ── LWW-map CRDT types ───────────────────────────────────────────────
// Each collection is a `ListCrdt` with a primary-key field.  A secondary
// index in `CrdtState` provides O(1) lookup by that key.

/// CRDT entry holding per-agent token-usage metrics.
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct TokenUsageCrdt {
    /// Unique key (e.g. `"coder-1:42_story_foo"`).
    pub agent_id: LwwRegisterCrdt<String>,
    pub story_id: LwwRegisterCrdt<String>,
    pub input_tokens: LwwRegisterCrdt<f64>,
    pub output_tokens: LwwRegisterCrdt<f64>,
    /// Unix timestamp (seconds) of this record.
    pub timestamp: LwwRegisterCrdt<f64>,
}

/// CRDT entry describing a merge job.
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct MergeJobCrdt {
    /// Unique key: the story being merged.
    pub story_id: LwwRegisterCrdt<String>,
    /// Status: `"pending"`, `"running"`, `"done"`, or `"failed"`.
    pub status: LwwRegisterCrdt<String>,
    pub started_at: LwwRegisterCrdt<f64>,
    pub finished_at: LwwRegisterCrdt<f64>,
    pub error: LwwRegisterCrdt<String>,
}

/// CRDT entry for a currently-running agent instance.
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct ActiveAgentCrdt {
    /// Unique key (e.g. `"coder-1"`).
    pub agent_id: LwwRegisterCrdt<String>,
    pub story_id: LwwRegisterCrdt<String>,
    pub node_id: LwwRegisterCrdt<String>,
    pub started_at: LwwRegisterCrdt<f64>,
}

/// CRDT entry describing a test job.
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct TestJobCrdt {
    /// Unique key: the story under test.
    pub story_id: LwwRegisterCrdt<String>,
    /// Status: `"pending"`, `"running"`, `"pass"`, or `"fail"`.
    pub status: LwwRegisterCrdt<String>,
    pub started_at: LwwRegisterCrdt<f64>,
    pub finished_at: LwwRegisterCrdt<f64>,
    pub output: LwwRegisterCrdt<String>,
}

/// CRDT entry holding per-node agent-throttle state.
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct AgentThrottleCrdt {
    /// Unique key: the node whose throttle this tracks.
    pub node_id: LwwRegisterCrdt<String>,
    /// Unix timestamp (seconds) of the throttle window start.
    pub window_start: LwwRegisterCrdt<f64>,
    /// Agent starts recorded in the current window.
    pub count: LwwRegisterCrdt<f64>,
    /// Maximum starts allowed per window.
    pub limit: LwwRegisterCrdt<f64>,
}

// ── LWW-map view types ───────────────────────────────────────────────

/// Snapshot of a single token-usage entry.
#[derive(Clone, Debug)]
pub struct TokenUsageView {
    pub agent_id: String,
    pub story_id: Option<String>,
    pub input_tokens: f64,
    pub output_tokens: f64,
    /// Unix timestamp (seconds).
    pub timestamp: f64,
}

/// Snapshot of a single merge-job entry.
#[derive(Clone, Debug)]
pub struct MergeJobView {
    pub story_id: String,
    pub status: String,
    pub started_at: f64,
    pub finished_at: Option<f64>,
    pub error: Option<String>,
}

/// Snapshot of a single active-agent entry.
#[derive(Clone, Debug)]
pub struct ActiveAgentView {
    pub agent_id: String,
    pub story_id: Option<String>,
    pub node_id: Option<String>,
    pub started_at: f64,
}

/// Snapshot of a single test-job entry.
#[derive(Clone, Debug)]
pub struct TestJobView {
    pub story_id: String,
    pub status: String,
    pub started_at: f64,
    pub finished_at: Option<f64>,
    pub output: Option<String>,
}

/// Snapshot of a single agent-throttle entry.
#[derive(Clone, Debug)]
pub struct AgentThrottleView {
    pub node_id: String,
    pub window_start: f64,
    pub count: f64,
    pub limit: f64,
}

#[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 node_metadata_fields_replicate_to_peer() {
        // Create two independent CRDT instances simulating two peers.
        let kp_a = make_keypair();
        let kp_b = make_keypair();
        let mut crdt_a = BaseCrdt::<PipelineDoc>::new(&kp_a);
        let mut crdt_b = BaseCrdt::<PipelineDoc>::new(&kp_b);

        // Peer A inserts a node entry with the new metadata fields.
        let node_json: JsonValue = serde_json::json!({
            "node_id": "aabbccdd",
            "address": "ws://192.168.1.10:3001/crdt-sync",
            "last_seen": 1_000_000.0_f64,
            "alive": true,
            "label": "builder-1",
            "assigned_project": "my-project",
            "last_seen_ms": 1_000_000_123.0_f64,
        })
        .into();

        let insert_op = crdt_a.doc.nodes.insert(ROOT_ID, node_json).sign(&kp_a);
        assert_eq!(crdt_a.apply(insert_op.clone()), OpState::Ok);

        // Replicate the insert op to peer B.
        assert_eq!(crdt_b.apply(insert_op), OpState::Ok);

        // Assert the node appeared on peer B.
        assert_eq!(crdt_b.doc.nodes.view().len(), 1);

        let node = &crdt_b.doc.nodes[0];
        assert_eq!(
            node.node_id.view(),
            JsonValue::String("aabbccdd".to_string())
        );
        assert_eq!(
            node.label.view(),
            JsonValue::String("builder-1".to_string())
        );
        assert_eq!(
            node.assigned_project.view(),
            JsonValue::String("my-project".to_string())
        );
        assert_eq!(node.last_seen_ms.view(), JsonValue::Number(1_000_000_123.0));

        // Peer A updates label and last_seen_ms via separate ops.
        let label_op = crdt_a.doc.nodes[0]
            .label
            .set("builder-1-renamed".to_string())
            .sign(&kp_a);
        let ms_op = crdt_a.doc.nodes[0]
            .last_seen_ms
            .set(2_000_000_000.0_f64)
            .sign(&kp_a);
        assert_eq!(crdt_a.apply(label_op.clone()), OpState::Ok);
        assert_eq!(crdt_a.apply(ms_op.clone()), OpState::Ok);

        // Replicate updates to peer B.
        assert_eq!(crdt_b.apply(label_op), OpState::Ok);
        assert_eq!(crdt_b.apply(ms_op), OpState::Ok);

        // Both peers converge to the same state.
        assert_eq!(
            crdt_b.doc.nodes[0].label.view(),
            JsonValue::String("builder-1-renamed".to_string())
        );
        assert_eq!(
            crdt_b.doc.nodes[0].last_seen_ms.view(),
            JsonValue::Number(2_000_000_000.0)
        );
        assert_eq!(
            crdt_a.doc.nodes[0].label.view(),
            crdt_b.doc.nodes[0].label.view()
        );
        assert_eq!(
            crdt_a.doc.nodes[0].last_seen_ms.view(),
            crdt_b.doc.nodes[0].last_seen_ms.view()
        );
    }

    #[test]
    fn node_metadata_deserializes_with_defaults_for_pre_existing_entries() {
        // Pre-existing entries without new fields should deserialize with sensible defaults.
        let kp = make_keypair();
        let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);

        // Insert a legacy node without the new fields.
        let legacy_json: JsonValue = serde_json::json!({
            "node_id": "legacy-node",
            "address": "ws://10.0.0.1:3001/crdt-sync",
            "last_seen": 500_000.0_f64,
            "alive": true,
        })
        .into();

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

        let node = &crdt.doc.nodes[0];
        // New fields default to Null (not set).
        assert_eq!(node.label.view(), JsonValue::Null);
        assert_eq!(node.assigned_project.view(), JsonValue::Null);
        assert_eq!(node.last_seen_ms.view(), JsonValue::Null);
        // Existing fields still work.
        assert_eq!(
            node.node_id.view(),
            JsonValue::String("legacy-node".to_string())
        );
        assert_eq!(node.alive.view(), JsonValue::Bool(true));
    }

    #[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"));
    }
}
