//! Typed NATS event bus for provisioning-daemon domain events.

use chrono::{DateTime, Utc};
use platform_nats::NatsBridge;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;

// ── Event types ───────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DaemonEvent {
    ToolInvoked(ToolInvokedEvent),
    ConfigChanged(ConfigChangedEvent),
    SessionOpened { session_id: String, at: DateTime<Utc> },
    SessionClosed { session_id: String, at: DateTime<Utc> },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInvokedEvent {
    pub tool: String,
    pub params: Value,
    pub result: ToolOutcome,
    pub duration_ms: u64,
    pub at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ToolOutcome {
    Ok { value: Value },
    Err { code: i32, message: String },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigChangedEvent {
    pub path: PathBuf,
    /// The type of filesystem change. Named `change` (not `kind`) to avoid
    /// collision with the `#[serde(tag = "kind")]` discriminant on `DaemonEvent`.
    pub change: ChangeKind,
    pub at: DateTime<Utc>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ChangeKind {
    Create,
    Modify,
    Remove,
}

// ── EventBus ─────────────────────────────────────────────────────────────────

/// Publishes domain events to NATS JetStream.
///
/// All subjects are relative to the bridge's configured prefix
/// (default: `provisioning.daemon.*`).
#[derive(Clone)]
pub struct EventBus {
    bridge: NatsBridge,
}

impl EventBus {
    pub fn new(bridge: NatsBridge) -> Self {
        Self { bridge }
    }

    pub async fn publish(&self, event: &DaemonEvent) -> anyhow::Result<()> {
        let subject = match event {
            DaemonEvent::ToolInvoked(_)  => "daemon.tools.invoked",
            DaemonEvent::ConfigChanged(_) => "daemon.config.changed",
            DaemonEvent::SessionOpened { .. } => "daemon.sessions.opened",
            DaemonEvent::SessionClosed { .. } => "daemon.sessions.closed",
        };
        self.bridge
            .publish_json(subject, event)
            .await
            .map(|_| ())
            .map_err(|e| anyhow::anyhow!("NATS publish to {subject}: {e}"))
    }

    /// Build a `ToolInvokedEvent` from a successful invocation.
    pub fn tool_ok(tool: &str, params: Value, value: Value, duration_ms: u64) -> DaemonEvent {
        DaemonEvent::ToolInvoked(ToolInvokedEvent {
            tool: tool.to_owned(),
            params,
            result: ToolOutcome::Ok { value },
            duration_ms,
            at: Utc::now(),
        })
    }

    /// Build a `ToolInvokedEvent` from a failed invocation.
    pub fn tool_err(
        tool: &str,
        params: Value,
        code: i32,
        message: String,
        duration_ms: u64,
    ) -> DaemonEvent {
        DaemonEvent::ToolInvoked(ToolInvokedEvent {
            tool: tool.to_owned(),
            params,
            result: ToolOutcome::Err { code, message },
            duration_ms,
            at: Utc::now(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn tool_ok_event_serializes() {
        let ev = EventBus::tool_ok("workspace_list", json!({}), json!({"workspaces":[]}), 42);
        let s = serde_json::to_string(&ev).unwrap();
        assert!(s.contains("\"kind\":\"tool_invoked\""));
        assert!(s.contains("\"status\":\"ok\""));
    }

    #[test]
    fn tool_err_event_serializes() {
        let ev = EventBus::tool_err("workspace_list", json!({}), -32000, "fail".into(), 5);
        let s = serde_json::to_string(&ev).unwrap();
        assert!(s.contains("\"status\":\"err\""));
    }

    #[test]
    fn config_changed_event_roundtrips() {
        let ev = DaemonEvent::ConfigChanged(ConfigChangedEvent {
            path: PathBuf::from("/tmp/test.ncl"),
            change: ChangeKind::Modify,
            at: Utc::now(),
        });
        let s = serde_json::to_string(&ev).unwrap();
        let back: DaemonEvent = serde_json::from_str(&s).unwrap();
        assert!(matches!(back, DaemonEvent::ConfigChanged(_)));
    }
}
