//! §21 Run lifecycle events emitted by core.
//!
//! Core emits these during run execution. The runtime layer observes them
//! to transition InputState, emit RuntimeEvents, and manage the run lifecycle.

use serde::{Deserialize, Serialize};

use crate::event::{AgentErrorClass, AgentErrorReport};

use super::identifiers::{InputId, RunId};
use super::run_receipt::RunBoundaryReceipt;

/// Typed run-failure payload emitted by core.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunFailureReport {
    pub class: AgentErrorClass,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<crate::event::AgentErrorReason>,
    pub message: String,
}

impl RunFailureReport {
    pub fn new(class: AgentErrorClass, message: impl Into<String>) -> Self {
        Self {
            class,
            reason: None,
            message: message.into(),
        }
    }

    pub fn from_agent_error_report(report: AgentErrorReport) -> Self {
        Self {
            class: report.class,
            reason: report.reason,
            message: report.message,
        }
    }

    pub fn display_message(&self) -> &str {
        &self.message
    }
}

/// Events emitted by core during run execution.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "event_type", rename_all = "snake_case")]
pub enum RunEvent {
    /// A run has started.
    RunStarted {
        /// The ID of the run.
        run_id: RunId,
    },

    /// A boundary primitive was applied successfully.
    BoundaryApplied {
        /// The ID of the run.
        run_id: RunId,
        /// The receipt proving application.
        receipt: RunBoundaryReceipt,
        /// Optional serialized session snapshot to persist atomically with the receipt.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        session_snapshot: Option<Vec<u8>>,
    },

    /// A run completed successfully.
    RunCompleted {
        /// The ID of the run.
        run_id: RunId,
        /// Input IDs that were consumed during this run.
        consumed_input_ids: Vec<InputId>,
    },

    /// A run failed with an error.
    RunFailed {
        /// The ID of the run.
        run_id: RunId,
        /// Typed failure details. The human-readable message is a display projection.
        failure: RunFailureReport,
        /// Whether this failure is recoverable (transient).
        recoverable: bool,
    },

    /// A run was cancelled through the live hard-interrupt capability.
    RunCancelled {
        /// The ID of the run.
        run_id: RunId,
    },
}

impl RunEvent {
    /// Get the run ID for this event.
    pub fn run_id(&self) -> &RunId {
        match self {
            RunEvent::RunStarted { run_id }
            | RunEvent::BoundaryApplied { run_id, .. }
            | RunEvent::RunCompleted { run_id, .. }
            | RunEvent::RunFailed { run_id, .. }
            | RunEvent::RunCancelled { run_id } => run_id,
        }
    }

    /// Get contributing input IDs if this event carries them.
    pub fn contributing_input_ids(&self) -> &[InputId] {
        match self {
            RunEvent::BoundaryApplied { receipt, .. } => &receipt.contributing_input_ids,
            RunEvent::RunCompleted {
                consumed_input_ids, ..
            } => consumed_input_ids,
            _ => &[],
        }
    }

    /// Get the display-only failure message if this event is `RunFailed`.
    pub fn failure_message(&self) -> Option<&str> {
        match self {
            RunEvent::RunFailed { failure, .. } => Some(failure.display_message()),
            _ => None,
        }
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::lifecycle::run_primitive::RunApplyBoundary;

    #[test]
    fn run_started_serde() {
        let event = RunEvent::RunStarted {
            run_id: RunId::new(),
        };
        let json = serde_json::to_value(&event).unwrap();
        assert_eq!(json["event_type"], "run_started");
        let parsed: RunEvent = serde_json::from_value(json).unwrap();
        assert_eq!(event.run_id(), parsed.run_id());
    }

    #[test]
    fn boundary_applied_serde() {
        let receipt = RunBoundaryReceipt {
            run_id: RunId::new(),
            boundary: RunApplyBoundary::RunStart,
            contributing_input_ids: vec![InputId::new()],
            conversation_digest: Some("digest".into()),
            message_count: 3,
            sequence: 0,
        };
        let event = RunEvent::BoundaryApplied {
            run_id: receipt.run_id.clone(),
            receipt: receipt.clone(),
            session_snapshot: Some(b"session".to_vec()),
        };
        let json = serde_json::to_value(&event).unwrap();
        assert_eq!(json["event_type"], "boundary_applied");
        let parsed: RunEvent = serde_json::from_value(json).unwrap();
        assert_eq!(
            parsed.contributing_input_ids(),
            receipt.contributing_input_ids.as_slice()
        );
    }

    #[test]
    fn run_completed_serde() {
        let ids = vec![InputId::new(), InputId::new()];
        let event = RunEvent::RunCompleted {
            run_id: RunId::new(),
            consumed_input_ids: ids.clone(),
        };
        let json = serde_json::to_value(&event).unwrap();
        assert_eq!(json["event_type"], "run_completed");
        let parsed: RunEvent = serde_json::from_value(json).unwrap();
        assert_eq!(parsed.contributing_input_ids(), ids.as_slice());
    }

    #[test]
    fn run_failed_serde() {
        let event = RunEvent::RunFailed {
            run_id: RunId::new(),
            failure: RunFailureReport::new(AgentErrorClass::Llm, "LLM timeout"),
            recoverable: true,
        };
        let json = serde_json::to_value(&event).unwrap();
        assert_eq!(json["event_type"], "run_failed");
        assert_eq!(json["failure"]["class"], "llm");
        assert_eq!(json["failure"]["message"], "LLM timeout");
        assert_eq!(json["recoverable"], true);
        let parsed: RunEvent = serde_json::from_value(json).unwrap();
        assert!(matches!(
            parsed,
            RunEvent::RunFailed {
                recoverable: true,
                ..
            }
        ));
    }

    #[test]
    fn run_cancelled_serde() {
        let event = RunEvent::RunCancelled {
            run_id: RunId::new(),
        };
        let json = serde_json::to_value(&event).unwrap();
        assert_eq!(json["event_type"], "run_cancelled");
        let parsed: RunEvent = serde_json::from_value(json).unwrap();
        assert!(matches!(parsed, RunEvent::RunCancelled { .. }));
    }

    #[test]
    fn run_event_run_id_accessor() {
        let run_id = RunId::new();
        let events = vec![
            RunEvent::RunStarted {
                run_id: run_id.clone(),
            },
            RunEvent::RunCompleted {
                run_id: run_id.clone(),
                consumed_input_ids: vec![],
            },
            RunEvent::RunFailed {
                run_id: run_id.clone(),
                failure: RunFailureReport::new(AgentErrorClass::Internal, "err"),
                recoverable: false,
            },
            RunEvent::RunCancelled {
                run_id: run_id.clone(),
            },
        ];
        for event in &events {
            assert_eq!(event.run_id(), &run_id);
        }
    }

    #[test]
    fn contributing_input_ids_empty_for_non_carrying_events() {
        let event = RunEvent::RunStarted {
            run_id: RunId::new(),
        };
        assert!(event.contributing_input_ids().is_empty());

        let event = RunEvent::RunFailed {
            run_id: RunId::new(),
            failure: RunFailureReport::new(AgentErrorClass::Internal, "err"),
            recoverable: false,
        };
        assert!(event.contributing_input_ids().is_empty());

        let event = RunEvent::RunCancelled {
            run_id: RunId::new(),
        };
        assert!(event.contributing_input_ids().is_empty());
    }

    #[test]
    fn run_failed_message_is_projection() {
        let event = RunEvent::RunFailed {
            run_id: RunId::new(),
            failure: RunFailureReport::new(AgentErrorClass::Hook, "denied"),
            recoverable: false,
        };
        assert_eq!(event.failure_message(), Some("denied"));
    }
}
