//! Read/write helpers for the `event_log` append-only list in the CRDT document.
//!
//! Every pipeline stage transition is appended as an [`EventLogEntryCrdt`][super::super::types::EventLogEntryCrdt]
//! entry.  Entries are never updated or tombstoned — the list is strictly grow-only.
//! Monotonic sequencing is computed at write time while holding the CRDT lock,
//! so `event_seq` values for a given sled are always contiguous and gap-free.

use bft_json_crdt::json_crdt::{JsonValue, *};
use bft_json_crdt::op::ROOT_ID;
use serde_json::json;

use super::super::state::{apply_and_persist, get_crdt};
use super::super::types::EventLogEntryCrdt;

/// `pipeline_event` value used to mark a gap sentinel entry in the event log.
///
/// A gap sentinel is appended when the event-log subscriber detects that the
/// broadcast channel dropped events (i.e. it received `RecvError::Lagged`).
/// The `from_stage` and `to_stage` fields encode the logical EventId range
/// `[from, to]` of the dropped events as decimal strings.
pub const GAP_PIPELINE_EVENT: &str = "EventStreamGap";

/// Raw event log entry extracted from the CRDT document.
///
/// All fields are decoded to Rust primitives; entries with a missing or
/// malformed `sled_id` are silently dropped by [`read_all_event_log_entries`].
pub struct EventLogEntryRaw {
    /// Monotonic sequence number for the recording sled (0-based).
    pub event_seq: u64,
    /// Hex-encoded Ed25519 public key of the sled that wrote this entry.
    pub sled_id: String,
    /// Unix timestamp (seconds) when the transition fired.
    pub timestamp: f64,
    /// Story ID of the work item that transitioned.
    pub story_id: String,
    /// Human-readable label of the stage before the transition.
    pub from_stage: String,
    /// Human-readable label of the stage after the transition.
    pub to_stage: String,
    /// String label of the `PipelineEvent` variant.
    pub pipeline_event: String,
}

/// Append a new event log entry to the CRDT, computing the monotonic `event_seq`
/// atomically while the CRDT lock is held.
///
/// No-ops silently when the CRDT is not yet initialised.
pub fn append_event_log_entry(
    sled_id: &str,
    timestamp: f64,
    story_id: &str,
    from_stage: &str,
    to_stage: &str,
    pipeline_event: &str,
) {
    let Some(state_mutex) = get_crdt() else {
        return;
    };
    let Ok(mut state) = state_mutex.lock() else {
        return;
    };

    // Count existing entries for this sled while holding the lock so the seq
    // is computed and used in the same critical section — no TOCTOU gap.
    let event_seq = state
        .crdt
        .doc
        .event_log
        .iter()
        .filter(|e| matches!(e.sled_id.view(), JsonValue::String(s) if s == sled_id))
        .count() as f64;

    // Append after the last existing entry so the list stays in insertion order.
    // Inserting after ROOT_ID would place each entry at the front (RGA semantics),
    // reversing the sequence; inserting after the current tail preserves order.
    let total_len = state.crdt.doc.event_log.view().len();
    let after = if total_len > 0 {
        super::list_id_at(&state.crdt.doc.event_log, total_len - 1).unwrap_or(ROOT_ID)
    } else {
        ROOT_ID
    };

    let entry: JsonValue = json!({
        "event_seq": event_seq,
        "sled_id": sled_id,
        "timestamp": timestamp,
        "story_id": story_id,
        "from_stage": from_stage,
        "to_stage": to_stage,
        "pipeline_event": pipeline_event,
    })
    .into();

    apply_and_persist(&mut state, |s| s.crdt.doc.event_log.insert(after, entry));
}

/// Append an `EventStreamGap` sentinel entry to the CRDT event log.
///
/// Called when the event-log broadcast subscriber detects that the channel
/// dropped events (`RecvError::Lagged`).  `from_id` and `to_id` are the
/// logical sequence numbers (in the per-sled event stream) of the first and
/// last dropped events respectively.  The sentinel itself also consumes one
/// CRDT `event_seq` slot so the monotonic counter remains contiguous across
/// the gap.
pub fn append_gap_log_entry(sled_id: &str, from_id: u64, to_id: u64) {
    let timestamp = chrono::Utc::now().timestamp() as f64;
    append_event_log_entry(
        sled_id,
        timestamp,
        "",
        &from_id.to_string(),
        &to_id.to_string(),
        GAP_PIPELINE_EVENT,
    );
}

/// Read all event log entries from the CRDT document.
///
/// Entries with a missing or empty `sled_id` are silently skipped.
/// Order reflects CRDT insertion order (RGA list semantics).
pub fn read_all_event_log_entries() -> Vec<EventLogEntryRaw> {
    let Some(state_mutex) = get_crdt() else {
        return Vec::new();
    };
    let Ok(state) = state_mutex.lock() else {
        return Vec::new();
    };
    state
        .crdt
        .doc
        .event_log
        .iter()
        .filter_map(extract_entry)
        .collect()
}

/// Convert a CRDT event log entry to its read-side representation.
fn extract_entry(e: &EventLogEntryCrdt) -> Option<EventLogEntryRaw> {
    let event_seq = match e.event_seq.view() {
        JsonValue::Number(n) => n as u64,
        _ => return None,
    };
    let sled_id = match e.sled_id.view() {
        JsonValue::String(s) if !s.is_empty() => s,
        _ => return None,
    };
    let timestamp = match e.timestamp.view() {
        JsonValue::Number(n) => n,
        _ => 0.0,
    };
    let story_id = match e.story_id.view() {
        JsonValue::String(s) => s,
        _ => String::new(),
    };
    let from_stage = match e.from_stage.view() {
        JsonValue::String(s) => s,
        _ => String::new(),
    };
    let to_stage = match e.to_stage.view() {
        JsonValue::String(s) => s,
        _ => String::new(),
    };
    let pipeline_event = match e.pipeline_event.view() {
        JsonValue::String(s) => s,
        _ => String::new(),
    };
    Some(EventLogEntryRaw {
        event_seq,
        sled_id,
        timestamp,
        story_id,
        from_stage,
        to_stage,
        pipeline_event,
    })
}
