//! Internal CRDT state struct, statics, initialisation, and central write primitive.
//!
//! This module is split into focused submodules:
//! - [`statics`]: broadcast channels and op-tracking statics
//! - [`indices`]: index-rebuild helpers for O(1) key lookup
//! - [`init`]: async startup and keypair persistence
//! - [`apply`]: write path (sign, apply, persist, broadcast)

use std::collections::{HashMap, HashSet};
use std::sync::{Mutex, OnceLock};

use bft_json_crdt::json_crdt::{BaseCrdt, SignedOp};
use bft_json_crdt::keypair::{Ed25519KeyPair, make_keypair};
use tokio::sync::{broadcast, mpsc};

use super::VectorClock;
use super::types::{CrdtEvent, PipelineDoc};

mod apply;
mod indices;
mod init;
mod statics;

#[cfg(test)]
mod tests;

// ── Re-exports for crdt_state siblings ──────────────────────────────

pub use init::init;
pub(crate) use init::{PersistMsg, flush_persistence};

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

pub(super) use apply::{apply_and_persist, emit_event};
pub(super) use indices::{
    rebuild_active_agent_index, rebuild_agent_throttle_index, rebuild_gateway_project_index,
    rebuild_index, rebuild_llm_session_index, rebuild_merge_job_index, rebuild_node_index,
    rebuild_test_job_index, rebuild_token_index,
};
pub(crate) use statics::{PERSIST_PENDING, all_ops_lock, vector_clock_lock};
pub(super) use statics::{SYNC_TX, track_op};

// ── CrdtState struct ─────────────────────────────────────────────────

/// Holds the core CRDT document, signing keypair, and all O(1) lookup indices.
pub(super) struct CrdtState {
    pub(super) crdt: BaseCrdt<PipelineDoc>,
    pub(super) keypair: Ed25519KeyPair,
    /// Maps story_id → index in the items ListCrdt for O(1) lookup.
    pub(super) index: HashMap<String, usize>,
    /// Maps node_id (hex) → index in the nodes ListCrdt for O(1) lookup.
    pub(super) node_index: HashMap<String, usize>,
    /// Maps agent_id → index in the tokens ListCrdt for O(1) lookup.
    pub(super) token_index: HashMap<String, usize>,
    /// Maps story_id → index in the merge_jobs ListCrdt for O(1) lookup.
    pub(super) merge_job_index: HashMap<String, usize>,
    /// Maps agent_id → index in the active_agents ListCrdt for O(1) lookup.
    pub(super) active_agent_index: HashMap<String, usize>,
    /// Maps story_id → index in the test_jobs ListCrdt for O(1) lookup.
    pub(super) test_job_index: HashMap<String, usize>,
    /// Maps node_id → index in the agent_throttle ListCrdt for O(1) lookup.
    pub(super) agent_throttle_index: HashMap<String, usize>,
    /// Maps project name → index in the gateway_projects ListCrdt for O(1) lookup.
    pub(super) gateway_project_index: HashMap<String, usize>,
    /// Maps session_id → index in the llm_sessions ListCrdt for O(1) lookup.
    pub(super) llm_session_index: HashMap<String, usize>,
    /// Channel sender for op persistence and drain signalling.
    pub(super) persist_tx: mpsc::UnboundedSender<init::PersistMsg>,
    /// Max sequence number seen across all ops during init() replay.
    ///
    /// Newly-created registers (post-init) must have their Lamport clock
    /// advanced to this floor so they don't re-emit low sequence numbers.
    pub(super) lamport_floor: u64,
    /// Story IDs permanently tombstoned via `evict_item`.
    ///
    /// `write_item` consults this set before inserting a new CRDT entry so
    /// that a concurrent or late-arriving write cannot resurrect a deleted
    /// story.  Rebuilt from the CRDT op log on restart.
    pub(super) tombstones: HashSet<String>,
}

// ── Singleton and accessor ───────────────────────────────────────────

/// Process-wide singleton holding the initialised [`CrdtState`].
pub(super) static CRDT_STATE: OnceLock<Mutex<CrdtState>> = OnceLock::new();

#[cfg(test)]
thread_local! {
    static CRDT_STATE_TL: OnceLock<Mutex<CrdtState>> = const { OnceLock::new() };
}

/// Returns a reference to the global [`CrdtState`] mutex, if initialised.
#[cfg(not(test))]
pub(super) fn get_crdt() -> Option<&'static Mutex<CrdtState>> {
    CRDT_STATE.get()
}

/// Returns the thread-local [`CrdtState`] if set, otherwise the global one (test variant).
#[cfg(test)]
pub(super) fn get_crdt() -> Option<&'static Mutex<CrdtState>> {
    let tl = CRDT_STATE_TL.with(|lock| {
        if lock.get().is_some() {
            Some(lock as *const OnceLock<Mutex<CrdtState>>)
        } else {
            None
        }
    });
    if let Some(ptr) = tl {
        // SAFETY: The thread-local lives as long as the thread, which outlives
        // any test using it.  We only need 'static for the return type.
        let lock = unsafe { &*ptr };
        lock.get()
    } else {
        CRDT_STATE.get()
    }
}

/// Initialise a minimal in-memory CRDT state for unit tests.
///
/// This avoids the async SQLite setup from `init()`.  Ops are sent to a
/// channel whose receiver is leaked (so nothing is persisted, but the channel
/// stays open and `apply_and_persist` succeeds silently).
/// Always resets all thread-local state so each call produces a clean slate —
/// no cross-test pollution when two tests share the same thread.
#[cfg(test)]
pub fn init_for_test() {
    let keypair = make_keypair();
    let crdt = BaseCrdt::<PipelineDoc>::new(&keypair);
    let (persist_tx, rx) = mpsc::unbounded_channel::<init::PersistMsg>();
    // Leak the receiver so the channel stays open: apply_and_persist
    // can then send without error, preventing [crdt_persist] WARNs
    // from racing with other tests that watch the global log buffer.
    std::mem::forget(rx);
    let fresh = CrdtState {
        crdt,
        keypair,
        index: HashMap::new(),
        node_index: HashMap::new(),
        token_index: HashMap::new(),
        merge_job_index: HashMap::new(),
        active_agent_index: HashMap::new(),
        test_job_index: HashMap::new(),
        agent_throttle_index: HashMap::new(),
        gateway_project_index: HashMap::new(),
        llm_session_index: HashMap::new(),
        persist_tx,
        lamport_floor: 0,
        tombstones: HashSet::new(),
    };
    CRDT_STATE_TL.with(|lock| {
        if let Some(mutex) = lock.get() {
            // Already set on this thread — replace contents so the second
            // (and subsequent) test on the same thread starts clean.
            *mutex.lock().unwrap() = fresh;
        } else {
            let _ = lock.set(Mutex::new(fresh));
        }
    });
    let _ = statics::CRDT_EVENT_TX.get_or_init(|| broadcast::channel::<CrdtEvent>(256).0);
    let _ = statics::SYNC_TX.get_or_init(|| broadcast::channel::<SignedOp>(1024).0);
    // Per-thread op journal + vector clock — always cleared so a second test
    // on the same thread cannot see ops written by the first.
    statics::ALL_OPS_TL.with(|lock| {
        if let Some(mutex) = lock.get() {
            mutex.lock().unwrap().clear();
        } else {
            let _ = lock.set(Mutex::new(Vec::new()));
        }
    });
    statics::VECTOR_CLOCK_TL.with(|lock| {
        if let Some(mutex) = lock.get() {
            mutex.lock().unwrap().clear();
        } else {
            let _ = lock.set(Mutex::new(VectorClock::new()));
        }
    });
}
