//! BusEvent → Langfuse ingestion mapping.
//!
//! Maps the common BusEvent kinds to Langfuse ingestion items (traces,
//! generations, spans). Events without a `scroll_id` are dropped —
//! Langfuse requires a trace id up-front.

// The items below are only wired into the stream when the `server`
// feature is on; the default-feature build keeps them for symmetry but
// does not reference them, so allow dead-code warnings there.
#![cfg_attr(not(feature = "server"), allow(dead_code))]

use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;

use dirigent_protocol::{streaming::BusEvent, Event};

/// A single Langfuse ingestion item.
///
/// Batched into `{ "batch": [...] }` in `LangfuseClient::ingest_batch`.
#[derive(Debug, Clone, Serialize)]
pub struct IngestItem {
    pub id: String, // UUIDv7
    pub timestamp: DateTime<Utc>,
    #[serde(rename = "type")]
    pub kind: IngestKind,
    pub body: serde_json::Value,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)] // SpanCreate/SpanUpdate reserved for future tool-call mapping
pub enum IngestKind {
    TraceCreate,
    GenerationCreate,
    GenerationUpdate,
    SpanCreate,
    SpanUpdate,
}

pub fn bus_event_to_items(bus_event: &BusEvent) -> Vec<IngestItem> {
    let Some(scroll_id) = bus_event.routing.scroll_id else {
        // No scroll_id binding yet — drop. Upstream callers may choose to
        // buffer pending events keyed by (connector_id, native_id) until
        // SessionRegistered arrives; Phase 4 scope: drop and log.
        return Vec::new();
    };

    let trace_id = scroll_id.to_string();
    let now = Utc::now();

    match &*bus_event.event {
        Event::SessionCreated { session, .. } => {
            // `session.title` is a `String`; fall back to the id if empty.
            let name = if session.title.is_empty() {
                session.id.clone()
            } else {
                session.title.clone()
            };
            vec![IngestItem {
                id: Uuid::now_v7().to_string(),
                timestamp: now,
                kind: IngestKind::TraceCreate,
                body: serde_json::json!({
                    "id": trace_id,
                    "name": name,
                }),
            }]
        }
        Event::MessageStarted { message, .. } => {
            vec![IngestItem {
                id: Uuid::now_v7().to_string(),
                timestamp: now,
                kind: IngestKind::GenerationCreate,
                body: serde_json::json!({
                    "id": message.id,
                    "traceId": trace_id,
                    "name": format!("{:?}", message.role),
                    "startTime": message.created_at,
                }),
            }]
        }
        Event::MessageCompleted { message, .. } => {
            vec![IngestItem {
                id: Uuid::now_v7().to_string(),
                timestamp: now,
                kind: IngestKind::GenerationUpdate,
                body: serde_json::json!({
                    "id": message.id,
                    "traceId": trace_id,
                    "endTime": now,
                    "output": serialize_content(&message.content),
                }),
            }]
        }
        Event::TurnComplete { .. } => Vec::new(), // captured by MessageCompleted
        // SessionUpdate::ToolCall* — would need a case-by-case mapping; out of
        // Phase 4 scope. Return empty for now.
        _ => Vec::new(),
    }
}

fn serialize_content(parts: &[dirigent_protocol::MessagePart]) -> serde_json::Value {
    serde_json::to_value(parts).unwrap_or(serde_json::Value::Null)
}

#[cfg(test)]
mod tests {
    use super::*;
    use dirigent_protocol::streaming::{BusEvent, EventKind, EventOrigin, EventRouting};
    use dirigent_protocol::{Event, Message, MessageRole, MessageStatus};
    use std::sync::Arc;

    fn make_bus_event_with_scroll(event: Event, scroll_id: Uuid) -> BusEvent {
        BusEvent {
            routing: EventRouting {
                scroll_id: Some(scroll_id),
                connector_uid: Some(Uuid::new_v4()),
                connector_id: Some("c".into()),
                native_session_id: Some("s".into()),
                kind: EventKind::Message,
            },
            origin: EventOrigin::Runtime,
            event: Arc::new(event),
        }
    }

    #[test]
    fn message_started_produces_generation_create() {
        let scroll_id = Uuid::new_v4();
        let msg = Message {
            id: "m1".into(),
            session_id: "s".into(),
            role: MessageRole::Assistant,
            created_at: chrono::Utc::now(),
            content: vec![],
            status: MessageStatus::Streaming,
            metadata: None,
        };
        let bus_event = make_bus_event_with_scroll(
            Event::MessageStarted {
                connector_id: "c".into(),
                message: msg,
            },
            scroll_id,
        );
        let items = bus_event_to_items(&bus_event);
        assert_eq!(items.len(), 1);
        assert!(matches!(items[0].kind, IngestKind::GenerationCreate));
    }

    #[test]
    fn no_scroll_id_drops_event() {
        let event = Event::Connected;
        let bus_event = BusEvent {
            routing: EventRouting::default(),
            origin: EventOrigin::Runtime,
            event: Arc::new(event),
        };
        let items = bus_event_to_items(&bus_event);
        assert_eq!(items.len(), 0);
    }

    #[test]
    fn unmapped_event_returns_empty() {
        // `Connected` is not one of our mapped variants even when a scroll_id
        // is bound → expect 0 items.
        let scroll_id = Uuid::new_v4();
        let bus_event = make_bus_event_with_scroll(Event::Connected, scroll_id);
        let items = bus_event_to_items(&bus_event);
        assert_eq!(items.len(), 0);
    }
}
