//! Agent inbox facade over the mediated input FIFO.
//!
//! The inbox is a bounded, per-agent view of incoming messages. It is a domain
//! port so infrastructure can implement it over the existing mediated mailbox
//! without creating a second competing queue.

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::ids::{AgentId, TaskId};
use crate::mailbox::TicketId;

/// Default bounded inbox capacity per agent.
pub const DEFAULT_AGENT_INBOX_CAPACITY: usize = 100;

/// Source of an [`InboxItem`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum InboxSource {
    /// Human operator.
    Human,
    /// Another agent.
    #[serde(rename_all = "camelCase")]
    Agent {
        /// Source agent.
        agent_id: AgentId,
    },
    /// A persisted background task completion.
    #[serde(rename_all = "camelCase")]
    BackgroundTask {
        /// Source task.
        task_id: TaskId,
    },
    /// IdeA system message.
    System,
}

impl InboxSource {
    /// Whether this source is lossless system work that must never be dropped.
    #[must_use]
    pub const fn is_lossless_system(self) -> bool {
        matches!(self, Self::BackgroundTask { .. } | Self::System)
    }
}

/// Kind of inbox item.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum InboxItemKind {
    /// Human message.
    UserMessage,
    /// Inter-agent delegation.
    AgentDelegation,
    /// Background task completion to deliver.
    BackgroundCompletion,
    /// Session resume notice.
    ResumeNotice,
}

impl InboxItemKind {
    /// Whether this kind is lossless system work that must never be dropped.
    #[must_use]
    pub const fn is_lossless_system(self) -> bool {
        matches!(self, Self::BackgroundCompletion | Self::ResumeNotice)
    }
}

/// Message queued for an agent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InboxItem {
    /// Stable queue item id. Reuses `TicketId` because the underlying FIFO is the
    /// mediated mailbox.
    pub id: TicketId,
    /// Target agent.
    pub agent_id: AgentId,
    /// Message source.
    pub source: InboxSource,
    /// Message kind.
    pub kind: InboxItemKind,
    /// Message body.
    pub body: String,
    /// Creation timestamp, epoch milliseconds.
    pub created_at_ms: u64,
    /// Optional correlation id, e.g. a task id.
    pub correlation_id: Option<String>,
}

impl InboxItem {
    /// Whether this item must remain persisted elsewhere rather than being lost
    /// on inbox overflow.
    #[must_use]
    pub const fn is_lossless_system(&self) -> bool {
        self.source.is_lossless_system() || self.kind.is_lossless_system()
    }
}

/// Result status of an enqueue.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum InboxReceiptStatus {
    /// Item was queued in the in-memory FIFO.
    Queued,
    /// Item was not queued because capacity was full, but remains durable in its
    /// authoritative store for later reconcile.
    Deferred,
}

/// Receipt returned by [`AgentInbox::enqueue_message`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InboxReceipt {
    /// Item id.
    pub item_id: TicketId,
    /// Target agent.
    pub agent_id: AgentId,
    /// FIFO depth after the operation.
    pub depth: usize,
    /// Enqueue outcome.
    pub status: InboxReceiptStatus,
}

/// Read-only inbox snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentInboxSnapshot {
    /// Target agent.
    pub agent_id: AgentId,
    /// Number of queued inbox items.
    pub depth: usize,
    /// FIFO-ordered items.
    pub items: Vec<InboxItem>,
}

/// Errors raised by [`AgentInbox`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum InboxError {
    /// The bounded inbox is full.
    #[error("agent inbox full for {agent_id} (capacity {capacity})")]
    InboxFull {
        /// Target agent.
        agent_id: AgentId,
        /// Configured capacity.
        capacity: usize,
    },
    /// The item target does not match the enqueue target.
    #[error("inbox item target {item_agent_id} does not match enqueue target {agent_id}")]
    AgentMismatch {
        /// Enqueue target.
        agent_id: AgentId,
        /// Item target.
        item_agent_id: AgentId,
    },
}

/// Bounded per-agent inbox facade.
pub trait AgentInbox: Send + Sync {
    /// Enqueues a message for `agent`.
    ///
    /// Normal messages fail with [`InboxError::InboxFull`] on overflow; lossless
    /// system/completion items return [`InboxReceiptStatus::Deferred`] instead.
    ///
    /// # Errors
    /// [`InboxError`] when the item is invalid or a normal message overflows.
    fn enqueue_message(&self, agent: AgentId, item: InboxItem) -> Result<InboxReceipt, InboxError>;

    /// Pops the next queued inbox item, if any.
    fn dequeue_next(&self, agent: AgentId) -> Option<InboxItem>;

    /// Returns the FIFO snapshot for `agent`.
    fn snapshot(&self, agent: AgentId) -> AgentInboxSnapshot;
}
