//! CRDT initialisation: async startup and keypair persistence.
//!
//! [`init`] opens the SQLite database, loads or creates the node keypair,
//! replays all persisted ops to reconstruct state, and spawns a background
//! persistence task.  It is safe to call only once; subsequent calls are
//! no-ops (guarded by [`super::CRDT_STATE`]).

use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::Mutex;
use std::sync::atomic::Ordering;

use bft_json_crdt::json_crdt::{BaseCrdt, CrdtNode, JsonValue, SignedOp};
use bft_json_crdt::keypair::{Ed25519KeyPair, make_keypair};
use sqlx::SqlitePool;
use sqlx::sqlite::SqliteConnectOptions;
use tokio::sync::{broadcast, mpsc, oneshot};

use super::super::VectorClock;
use super::super::hex;
use super::super::types::{CrdtEvent, PipelineDoc};
use super::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,
};
use super::statics::{ALL_OPS, CRDT_EVENT_TX, PERSIST_PENDING, SYNC_TX, VECTOR_CLOCK};
use super::{CRDT_STATE, CrdtState};
use crate::slog;

/// Message type for the persistence background channel.
pub(crate) enum PersistMsg {
    /// Persist this op to SQLite.
    Op(Box<SignedOp>),
    /// Drain: signal the sender after all preceding ops are committed.
    Flush(oneshot::Sender<()>),
}

/// Initialise the CRDT state layer.
///
/// Opens the SQLite database, loads or creates a node keypair, replays any
/// persisted ops to reconstruct state, and spawns a background persistence
/// task.  Safe to call only once; subsequent calls are no-ops.
#[allow(clippy::string_slice)] // op_id is hex::encode output (ASCII-only), &op_id[..12] is always valid
pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
    if CRDT_STATE.get().is_some() {
        return Ok(());
    }

    let options = SqliteConnectOptions::new()
        .filename(db_path)
        .create_if_missing(true);
    let pool = SqlitePool::connect_with(options).await?;
    sqlx::migrate!("./migrations").run(&pool).await?;

    // Load or create the node keypair.
    let keypair = load_or_create_keypair(&pool).await?;

    // Try to load a snapshot first — if one exists, we can skip replaying
    // the bulk of the op log and only replay ops that arrived after the
    // snapshot was taken.
    let snapshot_row: Option<(i64, i64, String)> =
        sqlx::query_as("SELECT at_seq, max_rowid, state_json FROM crdt_snapshot WHERE id = 1")
            .fetch_optional(&pool)
            .await?;

    let (mut crdt, all_ops_vec, vector_clock, lamport_floor) =
        if let Some((snap_seq, snap_max_rowid, state_json)) = snapshot_row {
            slog!(
                "[crdt] Loading snapshot (at_seq={}, max_rowid={})",
                snap_seq,
                snap_max_rowid
            );
            let kp = keypair.clone();
            let restore_result = tokio::task::spawn_blocking(move || {
                let doc: PipelineDoc = serde_json::from_str(&state_json)
                    .map_err(|e| format!("snapshot deserialize failed: {e}"))?;
                let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
                crdt.doc = doc;
                Ok::<_, String>(crdt)
            })
            .await
            .map_err(|e| sqlx::Error::Protocol(format!("snapshot restore panicked: {e}")))?
            .map_err(sqlx::Error::Protocol)?;

            // Replay only ops that arrived after the snapshot.
            let tail_rows: Vec<(String,)> =
                sqlx::query_as("SELECT op_json FROM crdt_ops WHERE rowid > ?1 ORDER BY rowid ASC")
                    .bind(snap_max_rowid)
                    .fetch_all(&pool)
                    .await?;

            let tail_count = tail_rows.len();
            let floor = snap_seq as u64;
            let replay_result = tokio::task::spawn_blocking(move || {
                let mut crdt = restore_result;
                let mut all_ops_vec = Vec::new();
                let mut vector_clock = VectorClock::new();
                let mut lamport_floor = floor;
                for (op_json,) in &tail_rows {
                    if let Ok(signed_op) = serde_json::from_str::<SignedOp>(op_json) {
                        let author_hex = hex::encode(&signed_op.author());
                        *vector_clock.entry(author_hex).or_insert(0) += 1;
                        lamport_floor = lamport_floor.max(signed_op.inner.seq);
                        crdt.apply(signed_op);
                        all_ops_vec.push(op_json.clone());
                    }
                }
                (crdt, all_ops_vec, vector_clock, lamport_floor)
            })
            .await
            .map_err(|e| sqlx::Error::Protocol(format!("tail replay panicked: {e}")))?;

            slog!("[crdt] Snapshot loaded, {} tail ops replayed", tail_count);
            replay_result
        } else {
            // No snapshot — full replay from scratch.
            let rows: Vec<(String,)> =
                sqlx::query_as("SELECT op_json FROM crdt_ops ORDER BY rowid ASC")
                    .fetch_all(&pool)
                    .await?;

            let row_count = rows.len();
            slog!(
                "[crdt] No snapshot found, replaying {} ops from scratch",
                row_count
            );

            let mut crdt = BaseCrdt::<PipelineDoc>::new(&keypair);
            let replay_result = tokio::task::spawn_blocking(move || {
                let mut all_ops_vec = Vec::with_capacity(row_count);
                let mut vector_clock = VectorClock::new();
                let mut lamport_floor: u64 = 0;
                for (op_json,) in &rows {
                    if let Ok(signed_op) = serde_json::from_str::<SignedOp>(op_json) {
                        let author_hex = hex::encode(&signed_op.author());
                        *vector_clock.entry(author_hex).or_insert(0) += 1;
                        lamport_floor = lamport_floor.max(signed_op.inner.seq);
                        crdt.apply(signed_op);
                        all_ops_vec.push(op_json.clone());
                    } else {
                        slog!("[crdt] Warning: failed to deserialize stored op");
                    }
                }
                (crdt, all_ops_vec, vector_clock, lamport_floor)
            })
            .await
            .map_err(|e| sqlx::Error::Protocol(format!("CRDT replay panicked: {e}")))?;

            // After a full replay, save a snapshot so subsequent restarts are fast.
            let (ref replay_crdt, _, _, replay_floor) = replay_result;
            save_snapshot(&pool, replay_crdt, replay_floor).await;

            replay_result
        };

    // Rebuild tombstone set and indices from the materialized state.
    let rebuild_result = tokio::task::spawn_blocking(move || {
        let tombstones: HashSet<String> = crdt
            .doc
            .items
            .ops
            .iter()
            .filter(|op| op.is_deleted)
            .filter_map(|op| op.content.as_ref())
            .filter_map(|item| match item.story_id.view() {
                JsonValue::String(s) if !s.is_empty() => Some(s),
                _ => None,
            })
            .collect();

        let index = rebuild_index(&crdt);
        let node_index = rebuild_node_index(&crdt);
        let token_index = rebuild_token_index(&crdt);
        let merge_job_index = rebuild_merge_job_index(&crdt);
        let active_agent_index = rebuild_active_agent_index(&crdt);
        let test_job_index = rebuild_test_job_index(&crdt);
        let agent_throttle_index = rebuild_agent_throttle_index(&crdt);
        let gateway_project_index = rebuild_gateway_project_index(&crdt);
        let llm_session_index = rebuild_llm_session_index(&crdt);

        crdt.doc.items.advance_seq(lamport_floor);
        crdt.doc.nodes.advance_seq(lamport_floor);
        crdt.doc.tokens.advance_seq(lamport_floor);
        crdt.doc.merge_jobs.advance_seq(lamport_floor);
        crdt.doc.active_agents.advance_seq(lamport_floor);
        crdt.doc.test_jobs.advance_seq(lamport_floor);
        crdt.doc.agent_throttle.advance_seq(lamport_floor);
        crdt.doc.gateway_projects.advance_seq(lamport_floor);
        crdt.doc.llm_sessions.advance_seq(lamport_floor);
        crdt.doc
            .gateway_config
            .active_project
            .advance_seq(lamport_floor);

        (
            crdt,
            tombstones,
            index,
            node_index,
            token_index,
            merge_job_index,
            active_agent_index,
            test_job_index,
            agent_throttle_index,
            gateway_project_index,
            llm_session_index,
        )
    })
    .await
    .map_err(|e| sqlx::Error::Protocol(format!("index rebuild panicked: {e}")))?;

    let (
        crdt,
        tombstones,
        index,
        node_index,
        token_index,
        merge_job_index,
        active_agent_index,
        test_job_index,
        agent_throttle_index,
        gateway_project_index,
        llm_session_index,
    ) = rebuild_result;

    let _ = ALL_OPS.set(Mutex::new(all_ops_vec));
    let _ = VECTOR_CLOCK.set(Mutex::new(vector_clock));

    slog!(
        "[crdt] Initialised: {} items indexed, {} nodes indexed, lamport_floor={}",
        index.len(),
        node_index.len(),
        lamport_floor,
    );

    // Spawn background persistence task.
    let (persist_tx, mut persist_rx) = mpsc::unbounded_channel::<PersistMsg>();

    tokio::spawn(async move {
        while let Some(msg) = persist_rx.recv().await {
            match msg {
                PersistMsg::Op(op) => {
                    let op = *op;
                    let op_json = match serde_json::to_string(&op) {
                        Ok(j) => j,
                        Err(e) => {
                            slog!("[crdt] Failed to serialize op: {e}");
                            PERSIST_PENDING.fetch_sub(1, Ordering::Relaxed);
                            continue;
                        }
                    };
                    let op_id = hex::encode(&op.id());
                    let seq = op.inner.seq as i64;
                    let now = chrono::Utc::now().to_rfc3339();

                    let result = sqlx::query(
                        "INSERT INTO crdt_ops (op_id, seq, op_json, created_at) \
                         VALUES (?1, ?2, ?3, ?4) \
                         ON CONFLICT(op_id) DO NOTHING",
                    )
                    .bind(&op_id)
                    .bind(seq)
                    .bind(&op_json)
                    .bind(&now)
                    .execute(&pool)
                    .await;

                    if let Err(e) = result {
                        slog!("[crdt] Failed to persist op {}: {e}", &op_id[..12]);
                    }
                    PERSIST_PENDING.fetch_sub(1, Ordering::Relaxed);
                }
                PersistMsg::Flush(reply) => {
                    // All ops queued before this message have already been processed.
                    let _ = reply.send(());
                }
            }
        }
    });

    let state = CrdtState {
        crdt,
        keypair,
        index,
        node_index,
        token_index,
        merge_job_index,
        active_agent_index,
        test_job_index,
        agent_throttle_index,
        gateway_project_index,
        llm_session_index,
        persist_tx,
        lamport_floor,
        tombstones,
    };

    let _ = CRDT_STATE.set(Mutex::new(state));

    // Initialise the CRDT event broadcast channel.
    let (event_tx, _) = broadcast::channel::<CrdtEvent>(256);
    let _ = CRDT_EVENT_TX.set(event_tx);

    // Initialise the sync broadcast channel for outgoing ops.
    let (sync_tx, _) = broadcast::channel::<SignedOp>(1024);
    let _ = SYNC_TX.set(sync_tx);

    Ok(())
}

/// Signal the persistence background task to drain and wait until all currently-queued
/// ops have been written to SQLite, or until `timeout` elapses.
///
/// Because the persistence channel is FIFO, a `Flush` sentinel processed by the task
/// guarantees that every `Op` sent before it has already been committed.  On timeout a
/// warning is logged with the queue depth so regressions are visible in logs.
pub(crate) async fn flush_persistence(timeout: std::time::Duration) {
    let Some(state_mutex) = super::get_crdt() else {
        return;
    };
    let persist_tx = {
        let Ok(state) = state_mutex.lock() else {
            return;
        };
        state.persist_tx.clone()
    };
    let pending_at_send = PERSIST_PENDING.load(Ordering::Relaxed);
    let (tx, rx) = oneshot::channel();
    if persist_tx.send(PersistMsg::Flush(tx)).is_err() {
        slog!("[rebuild] Persistence channel closed — skipping flush");
        return;
    }
    match tokio::time::timeout(timeout, rx).await {
        Ok(_) => {
            slog!("[rebuild] Persistence channel drained ({pending_at_send} ops flushed)");
        }
        Err(_) => {
            let pending_now = PERSIST_PENDING.load(Ordering::Relaxed);
            slog!(
                "[rebuild] WARNING: persistence flush timed out after {}ms; \
                 queue_depth_at_send={pending_at_send} queue_depth_now={pending_now}",
                timeout.as_millis()
            );
        }
    }
}

/// Load or create the Ed25519 keypair used by this node.
async fn load_or_create_keypair(pool: &SqlitePool) -> Result<Ed25519KeyPair, sqlx::Error> {
    let row: Option<(Vec<u8>,)> =
        sqlx::query_as("SELECT seed FROM crdt_node_identity WHERE id = 1")
            .fetch_optional(pool)
            .await?;

    if let Some((seed,)) = row {
        // Reconstruct from stored seed.  The seed is the 32-byte private key.
        if let Ok(arr) = TryInto::<[u8; 32]>::try_into(seed) {
            return Ok(Ed25519KeyPair::from_bytes(&arr));
        }
        slog!("[crdt] Stored keypair invalid, regenerating");
    }

    let kp = make_keypair();
    let seed = kp.to_bytes().to_vec();
    sqlx::query("INSERT INTO crdt_node_identity (id, seed) VALUES (1, ?1) ON CONFLICT(id) DO UPDATE SET seed = excluded.seed")
        .bind(&seed)
        .execute(pool)
        .await?;

    Ok(kp)
}

/// Serialize the current CRDT document state into the `crdt_snapshot` table
/// and back up the database file.  Errors are logged but not propagated —
/// a failed snapshot just means the next restart will do a full replay.
async fn save_snapshot(pool: &SqlitePool, crdt: &BaseCrdt<PipelineDoc>, lamport_floor: u64) {
    // Find the highest rowid currently in crdt_ops — ops with rowid <= this
    // value are already captured in the snapshot.
    let max_rowid: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(rowid), 0) FROM crdt_ops")
        .fetch_one(pool)
        .await
        .unwrap_or(0);

    let doc_ref = &crdt.doc;
    let json = match serde_json::to_string(doc_ref) {
        Ok(j) => j,
        Err(e) => {
            slog!("[crdt] Failed to serialize snapshot: {e}");
            return;
        }
    };

    let json_len = json.len();
    let now = chrono::Utc::now().to_rfc3339();

    // Back up the database before writing the snapshot row.
    if let Some(db_path) = pool_path(pool) {
        let bak_path = format!("{db_path}.bak");
        if let Err(e) = tokio::fs::copy(&db_path, &bak_path).await {
            slog!("[crdt] DB backup failed: {e}");
        } else {
            slog!("[crdt] DB backed up to {bak_path}");
        }
    }

    let result = sqlx::query(
        "INSERT OR REPLACE INTO crdt_snapshot (id, at_seq, max_rowid, state_json, created_at) \
         VALUES (1, ?1, ?2, ?3, ?4)",
    )
    .bind(lamport_floor as i64)
    .bind(max_rowid)
    .bind(&json)
    .bind(&now)
    .execute(pool)
    .await;

    match result {
        Ok(_) => slog!(
            "[crdt] Snapshot saved: at_seq={}, max_rowid={}, json={}B",
            lamport_floor,
            max_rowid,
            json_len
        ),
        Err(e) => slog!("[crdt] Failed to save snapshot: {e}"),
    }
}

/// Extract the filesystem path from a SqlitePool's connect options.
fn pool_path(pool: &SqlitePool) -> Option<String> {
    use sqlx::ConnectOptions;
    let opts = pool.connect_options();
    let filename = opts.get_filename();
    filename.to_str().map(|s| s.to_string())
}
