//! Public sync-broadcast API and remote-op ingestion.

#![allow(unused_imports, dead_code)]
use std::collections::HashMap;
use std::sync::atomic::Ordering;

use super::hex;
use bft_json_crdt::json_crdt::*;
use bft_json_crdt::op::ROOT_ID;
use tokio::sync::broadcast;

use super::VectorClock;
use super::state::{
    PERSIST_PENDING, PersistMsg, SYNC_TX, all_ops_lock, apply_and_persist, emit_event, get_crdt,
    rebuild_active_agent_index, rebuild_agent_throttle_index, rebuild_index,
    rebuild_merge_job_index, rebuild_node_index, rebuild_test_job_index, rebuild_token_index,
    track_op, vector_clock_lock,
};
use super::types::{CrdtEvent, PipelineDoc};
use crate::slog;

/// Subscribe to locally-created CRDT ops for sync replication.
///
/// Each `SignedOp` broadcast here was created by *this* node and should be
/// forwarded to connected peers.  Returns `None` before `init()`.
pub fn subscribe_ops() -> Option<broadcast::Receiver<SignedOp>> {
    SYNC_TX.get().map(|tx| tx.subscribe())
}

/// Return all persisted `SignedOp`s in causal order (oldest first).
///
/// Used during initial sync handshake so a newly-connected peer can
/// reconstruct the full CRDT state.  Returns `None` before `init()`.
pub fn all_ops_json() -> Option<Vec<String>> {
    all_ops_lock().map(|m| m.lock().unwrap().clone())
}

/// Return this node's current vector clock.
///
/// The clock maps each author's hex-encoded Ed25519 public key to the count
/// of ops received from that author.  A connecting peer sends its clock so
/// the other side can compute which ops are missing via [`ops_since`].
///
/// Returns `None` before `init()`.
pub fn our_vector_clock() -> Option<VectorClock> {
    vector_clock_lock().map(|m| m.lock().unwrap().clone())
}

/// Return only the ops that a peer with the given `peer_clock` is missing.
///
/// Iterates the local op journal and, for each author, skips the first N ops
/// (where N = `peer_clock[author]`) and returns the rest.  An empty peer
/// clock returns all ops (full sync for new nodes).
///
/// Returns `None` before `init()`.
pub fn ops_since(peer_clock: &VectorClock) -> Option<Vec<String>> {
    let all = all_ops_lock()?.lock().ok()?;
    let mut author_counts: HashMap<String, u64> = HashMap::new();
    let mut result = Vec::new();

    for op_json in all.iter() {
        if let Ok(signed_op) = serde_json::from_str::<SignedOp>(op_json) {
            let author_hex = super::hex::encode(&signed_op.author());
            let count = author_counts.entry(author_hex.clone()).or_insert(0);
            *count += 1;

            let peer_has = peer_clock.get(&author_hex).copied().unwrap_or(0);
            if *count > peer_has {
                result.push(op_json.clone());
            }
        }
    }

    Some(result)
}

// ── Remote op ingestion (from sync peers) ───────────────────────────

/// Apply a `SignedOp` received from a remote peer.
///
/// The op is validated, applied to the local CRDT, persisted to SQLite,
/// and any resulting stage transitions are broadcast as [`CrdtEvent`]s.
/// Unlike `apply_and_persist`, this does **not** re-broadcast the op on
/// the sync channel (to avoid infinite echo loops).
///
/// Returns `true` if the op was new and applied, `false` if it was a
/// duplicate or failed validation.
pub fn apply_remote_op(op: SignedOp) -> bool {
    let Some(state_mutex) = get_crdt() else {
        return false;
    };
    let Ok(mut state) = state_mutex.lock() else {
        return false;
    };

    // Snapshot stage state before applying so we can detect transitions.
    let pre_stages: HashMap<String, String> = state
        .index
        .iter()
        .filter_map(|(sid, &idx)| match state.crdt.doc.items[idx].stage.view() {
            JsonValue::String(s) => Some((sid.clone(), s)),
            _ => None,
        })
        .collect();

    let result = state.crdt.apply(op.clone());

    // Self-loop guard: op was already applied (came back via echo from peer).
    // Return false immediately — do not re-persist or re-add to ALL_OPS.
    if result == bft_json_crdt::json_crdt::OpState::AlreadySeen {
        return false;
    }

    if result != bft_json_crdt::json_crdt::OpState::Ok
        && result != bft_json_crdt::json_crdt::OpState::MissingCausalDependencies
    {
        return false;
    }

    // Persist the op.
    if state
        .persist_tx
        .send(PersistMsg::Op(Box::new(op.clone())))
        .is_ok()
    {
        PERSIST_PENDING.fetch_add(1, Ordering::Relaxed);
    } else {
        crate::slog_error!(
            "[crdt] Failed to send remote op to persist task; persist task may be dead. \
             In-memory state is now ahead of persisted state."
        );
    }

    // Track in ALL_OPS + VECTOR_CLOCK.
    if let Ok(json) = serde_json::to_string(&op) {
        track_op(&op, json);
    }

    // Rebuild indices (new items, nodes, or LWW-map entries may have been inserted).
    state.index = rebuild_index(&state.crdt);
    state.node_index = rebuild_node_index(&state.crdt);
    state.token_index = rebuild_token_index(&state.crdt);
    state.merge_job_index = rebuild_merge_job_index(&state.crdt);
    state.active_agent_index = rebuild_active_agent_index(&state.crdt);
    state.test_job_index = rebuild_test_job_index(&state.crdt);
    state.agent_throttle_index = rebuild_agent_throttle_index(&state.crdt);

    // Propagate tombstones from remotely-received delete ops.
    //
    // The tombstone set is normally populated only by the local evict_item
    // call.  Without this step, any node that receives a delete op from a
    // peer would leave the story absent from state.index but NOT in
    // state.tombstones — so a concurrent write_item call on this node could
    // re-insert the story, undoing the remote delete.
    if op.inner.is_deleted {
        let new_tombstones: Vec<String> = state
            .crdt
            .doc
            .items
            .ops
            .iter()
            .filter(|o| o.is_deleted)
            .filter_map(|o| o.content.as_ref())
            .filter_map(|item| match item.story_id.view() {
                JsonValue::String(s) if !s.is_empty() => Some(s),
                _ => None,
            })
            .collect();
        state.tombstones.extend(new_tombstones);
    }

    // Detect and broadcast stage transitions.
    for (sid, &idx) in &state.index {
        let new_stage_str = match state.crdt.doc.items[idx].stage.view() {
            JsonValue::String(s) => s,
            _ => continue,
        };
        let old_stage_str = pre_stages.get(sid).cloned();
        let changed = old_stage_str.as_deref() != Some(&new_stage_str);
        if changed {
            // Storage seam: convert the raw CRDT stage strings to typed Stage values here.
            let Some(to_stage) = crate::pipeline_state::Stage::from_dir(&new_stage_str) else {
                continue;
            };
            let from_stage = old_stage_str.and_then(|s| crate::pipeline_state::Stage::from_dir(&s));
            let name = match state.crdt.doc.items[idx].name.view() {
                JsonValue::String(s) => s.clone(),
                _ => String::new(),
            };
            emit_event(CrdtEvent {
                story_id: sid.clone(),
                from_stage,
                to_stage,
                name,
            });
        }
    }

    true
}

#[cfg(test)]
mod tests {
    use super::super::state::init_for_test;
    use super::super::types::{NodePresenceCrdt, PipelineItemCrdt};
    use super::super::write::write_item_str;
    use super::*;
    use bft_json_crdt::json_crdt::OpState;
    use bft_json_crdt::keypair::{Ed25519KeyPair, make_keypair};
    use bft_json_crdt::op::ROOT_ID;
    use serde_json::json;

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

        let item: JsonValue = json!({
            "story_id": "60_story_serde",
            "stage": "1_backlog",
            "name": "Serde Test",
            "agent": "",
            "retry_count": 0.0,
            "blocked": false,
            "depends_on": "",
            "claimed_by": "",
            "claimed_at": 0.0,
        })
        .into();

        let op = crdt.doc.items.insert(ROOT_ID, item).sign(&kp);
        let json_str = serde_json::to_string(&op).unwrap();
        let deserialized: SignedOp = serde_json::from_str(&json_str).unwrap();

        assert_eq!(op.id(), deserialized.id());
        assert_eq!(op.inner.seq, deserialized.inner.seq);
    }

    #[test]
    fn apply_remote_op_returns_false_when_not_initialised() {
        // Without the global CRDT state, apply_remote_op should return false.
        let kp = make_keypair();
        let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
        let item: JsonValue = serde_json::json!({
            "story_id": "80_story_remote",
            "stage": "1_backlog",
            "name": "Remote",
            "agent": "",
            "retry_count": 0.0,
            "blocked": false,
            "depends_on": "",
            "claimed_by": "",
            "claimed_at": 0.0,
        })
        .into();
        let op = crdt
            .doc
            .items
            .insert(bft_json_crdt::op::ROOT_ID, item)
            .sign(&kp);
        // This uses the global state which may not be initialised in tests.
        let _ = apply_remote_op(op);
    }

    #[test]
    fn signed_op_survives_sync_serialization_roundtrip() {
        // Verify that a SignedOp serialised to JSON and back produces
        // the same op (critical for the sync wire protocol).
        let kp = make_keypair();
        let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
        let item: JsonValue = serde_json::json!({
            "story_id": "90_story_wire",
            "stage": "2_current",
            "name": "Wire Test",
            "agent": "coder",
            "retry_count": 1.0,
            "blocked": false,
            "depends_on": "[10]",
            "claimed_by": "",
            "claimed_at": 0.0,
        })
        .into();
        let op = crdt
            .doc
            .items
            .insert(bft_json_crdt::op::ROOT_ID, item)
            .sign(&kp);

        let json1 = serde_json::to_string(&op).unwrap();
        let roundtripped: SignedOp = serde_json::from_str(&json1).unwrap();
        let json2 = serde_json::to_string(&roundtripped).unwrap();

        assert_eq!(json1, json2);
        assert_eq!(op.id(), roundtripped.id());
        assert_eq!(op.inner.seq, roundtripped.inner.seq);
        assert_eq!(op.author(), roundtripped.author());
    }

    #[test]
    fn sync_broadcast_channel_round_trip() {
        let (tx, mut rx) = broadcast::channel::<SignedOp>(16);
        let kp = make_keypair();
        let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
        let item: JsonValue = serde_json::json!({
            "story_id": "95_story_sync_bcast",
            "stage": "1_backlog",
            "name": "",
            "agent": "",
            "retry_count": 0.0,
            "blocked": false,
            "depends_on": "",
            "claimed_by": "",
            "claimed_at": 0.0,
        })
        .into();
        let op = crdt
            .doc
            .items
            .insert(bft_json_crdt::op::ROOT_ID, item)
            .sign(&kp);
        tx.send(op.clone()).unwrap();

        let received = rx.try_recv().unwrap();
        assert_eq!(received.id(), op.id());
    }

    fn make_ops(
        kp: &Ed25519KeyPair,
        crdt: &mut BaseCrdt<PipelineDoc>,
        count: usize,
        prefix: &str,
    ) -> Vec<(SignedOp, String)> {
        let mut ops = Vec::new();
        for i in 0..count {
            let item: JsonValue = json!({
                "story_id": format!("{prefix}_{i}"),
                "stage": "1_backlog",
                "name": format!("Item {i}"),
                "agent": "",
                "retry_count": 0.0,
                "blocked": false,
                "depends_on": "",
                "claimed_by": "",
                "claimed_at": 0.0,
            })
            .into();
            let op = crdt.doc.items.insert(ROOT_ID, item).sign(kp);
            crdt.apply(op.clone());
            let json = serde_json::to_string(&op).unwrap();
            ops.push((op, json));
        }
        ops
    }

    fn build_clock(ops: &[(SignedOp, String)]) -> VectorClock {
        let mut clock = VectorClock::new();
        for (op, _) in ops {
            let author = hex::encode(&op.author());
            *clock.entry(author).or_insert(0) += 1;
        }
        clock
    }

    fn local_ops_since(all_ops: &[(SignedOp, String)], peer_clock: &VectorClock) -> Vec<String> {
        let mut author_counts: HashMap<String, u64> = HashMap::new();
        let mut result = Vec::new();
        for (op, json) in all_ops {
            let author = hex::encode(&op.author());
            let count = author_counts.entry(author.clone()).or_insert(0);
            *count += 1;
            let peer_has = peer_clock.get(&author).copied().unwrap_or(0);
            if *count > peer_has {
                result.push(json.clone());
            }
        }
        result
    }

    #[test]
    fn delta_sync_low_bandwidth_fully_caught_up() {
        let kp_a = make_keypair();
        let mut crdt_a = BaseCrdt::<PipelineDoc>::new(&kp_a);

        let ops_a = make_ops(&kp_a, &mut crdt_a, 100, "631_low");

        // B has already seen all 100 ops (its clock matches A's journal).
        let clock_b = build_clock(&ops_a);

        // Delta should be empty.
        let delta = local_ops_since(&ops_a, &clock_b);
        assert_eq!(
            delta.len(),
            0,
            "caught-up peer should receive 0 ops, got {}",
            delta.len()
        );
    }

    #[test]
    fn delta_sync_mid_stream_partial_catch_up() {
        let kp_a = make_keypair();
        let mut crdt_a = BaseCrdt::<PipelineDoc>::new(&kp_a);

        // Phase 1: 100 ops that B has seen.
        let ops_phase1 = make_ops(&kp_a, &mut crdt_a, 100, "631_mid1");
        let clock_b = build_clock(&ops_phase1);

        // Phase 2: 50 more ops that B missed.
        let ops_phase2 = make_ops(&kp_a, &mut crdt_a, 50, "631_mid2");

        // A's full journal is phase1 + phase2.
        let mut all_ops_a: Vec<(SignedOp, String)> = ops_phase1;
        all_ops_a.extend(ops_phase2);

        let delta = local_ops_since(&all_ops_a, &clock_b);
        assert_eq!(
            delta.len(),
            50,
            "peer should receive exactly 50 missed ops, got {}",
            delta.len()
        );
    }

    #[test]
    fn delta_sync_new_node_receives_all_ops() {
        let kp_a = make_keypair();
        let mut crdt_a = BaseCrdt::<PipelineDoc>::new(&kp_a);

        let ops_phase1 = make_ops(&kp_a, &mut crdt_a, 100, "631_new1");
        let ops_phase2 = make_ops(&kp_a, &mut crdt_a, 50, "631_new2");

        let mut all_ops_a: Vec<(SignedOp, String)> = ops_phase1;
        all_ops_a.extend(ops_phase2);

        // Empty clock = new node.
        let empty_clock = VectorClock::new();
        let delta = local_ops_since(&all_ops_a, &empty_clock);
        assert_eq!(
            delta.len(),
            150,
            "new node should receive all 150 ops, got {}",
            delta.len()
        );
    }

    #[test]
    fn delta_sync_multi_author() {
        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);

        let ops_a = make_ops(&kp_a, &mut crdt_a, 30, "631_ma_a");
        let ops_b = make_ops(&kp_b, &mut crdt_b, 20, "631_ma_b");

        // Combined journal on a hypothetical server.
        let mut all_ops: Vec<(SignedOp, String)> = ops_a.clone();
        all_ops.extend(ops_b);

        // Peer has seen all of A's ops but none of B's.
        let mut peer_clock = VectorClock::new();
        let author_a_hex = hex::encode(&kp_a.verifying_key().to_bytes());
        peer_clock.insert(author_a_hex, 30);

        let delta = local_ops_since(&all_ops, &peer_clock);
        assert_eq!(
            delta.len(),
            20,
            "peer should receive 20 ops from author B, got {}",
            delta.len()
        );
    }

    #[test]
    fn build_vector_clock_from_ops() {
        let kp = make_keypair();
        let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
        let ops = make_ops(&kp, &mut crdt, 10, "631_vc");

        let clock = build_clock(&ops);
        let author_hex = hex::encode(&kp.verifying_key().to_bytes());

        assert_eq!(clock.len(), 1, "single author should produce 1 clock entry");
        assert_eq!(clock[&author_hex], 10, "clock should show 10 ops");
    }

    #[test]
    fn clock_message_serialization_roundtrip() {
        let mut clock = VectorClock::new();
        clock.insert("aabbcc".to_string(), 42);
        clock.insert("ddeeff".to_string(), 7);

        let json = serde_json::to_value(&clock).unwrap();
        assert!(json.is_object());
        let deserialized: VectorClock = serde_json::from_value(json).unwrap();
        assert_eq!(deserialized["aabbcc"], 42);
        assert_eq!(deserialized["ddeeff"], 7);
    }

    /// Regression for story 889 AC4: applying a remote delete op must update the
    /// local tombstone set so that a subsequent write_item call cannot resurrect
    /// the deleted story.  Before the fix, apply_remote_op rebuilt indices but
    /// left state.tombstones empty, allowing write_item to re-insert the item.
    #[test]
    fn remote_delete_op_prevents_resurrection() {
        use super::super::read::read_item;
        use super::super::types::PipelineDoc;

        let story_id = "889_story_remote_del_resurrect";

        // Build insert + delete ops on a standalone CRDT (the "remote" node).
        let kp = make_keypair();
        let mut remote = BaseCrdt::<PipelineDoc>::new(&kp);

        let item: JsonValue = json!({
            "story_id": story_id,
            "stage": "1_backlog",
            "name": "Remote Delete Resurrection Test",
            "agent": "",
            "retry_count": 0.0,
            "blocked": false,
            "depends_on": "",
            "claimed_by": "",
            "claimed_at": 0.0,
            "merged_at": 0.0,
        })
        .into();

        let signed_insert = remote.doc.items.insert(ROOT_ID, item).sign(&kp);
        remote.apply(signed_insert.clone());

        let insert_op_id = signed_insert.id();
        let signed_delete = remote.doc.items.delete(insert_op_id).sign(&kp);

        // Feed the insert op to the local test singleton so the story exists here.
        init_for_test();
        let accepted = apply_remote_op(signed_insert);
        assert!(accepted, "remote insert must be accepted");
        assert!(
            read_item(story_id).is_some(),
            "story must be active after remote insert"
        );

        // Apply the remote delete op.
        let accepted = apply_remote_op(signed_delete);
        assert!(accepted, "remote delete op must be accepted");
        assert!(
            read_item(story_id).is_none(),
            "story must be gone after remote delete op"
        );

        // Attempt resurrection via write_item — must be rejected by tombstone check.
        write_item_str(story_id, "1_backlog", Some("Resurrected"), None, None, None);
        assert!(
            read_item(story_id).is_none(),
            "tombstoned story must not be resurrected by write_item after remote delete"
        );
    }
}
