//! Runtime-neutral, size-bounded messages shared by agent subsystems.

use libremetaverse_types::UUID;
use std::error::Error;
use std::fmt;
use std::ops::Deref;

/// Absolute body bound accepted at any agent boundary (8 MiB).
pub const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
/// Absolute UTF-8 message bound accepted at any agent boundary (64 KiB).
pub const MAX_MESSAGE_BYTES: usize = 64 * 1024;
/// Absolute messages retained in one normalized conversation.
pub const MAX_CONVERSATION_MESSAGES: usize = 256;
/// Absolute number of proposed calls returned for one LLM request.
pub const MAX_TOOL_CALLS: usize = 64;
/// Bound for a tool name or stable identifier.
pub const MAX_IDENTIFIER_BYTES: usize = 128;
/// Bound for JSON arguments attached to one proposed tool call (256 KiB).
pub const MAX_TOOL_ARGUMENT_BYTES: usize = 256 * 1024;
/// Bound for human-readable diagnostics published through observable events.
pub const MAX_OBSERVABLE_DETAIL_BYTES: usize = 2 * 1024;

/// Typed failure produced before an oversized boundary value is admitted.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BoundaryError {
    field: &'static str,
    actual: usize,
    maximum: usize,
    problem: BoundaryProblem,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum BoundaryProblem {
    Empty,
    TooLarge,
    InvalidJson,
}

impl BoundaryError {
    const fn empty(field: &'static str, maximum: usize) -> Self {
        Self {
            field,
            actual: 0,
            maximum,
            problem: BoundaryProblem::Empty,
        }
    }

    const fn too_large(field: &'static str, actual: usize, maximum: usize) -> Self {
        Self {
            field,
            actual,
            maximum,
            problem: BoundaryProblem::TooLarge,
        }
    }

    const fn invalid_json(field: &'static str, actual: usize, maximum: usize) -> Self {
        Self {
            field,
            actual,
            maximum,
            problem: BoundaryProblem::InvalidJson,
        }
    }
}

impl fmt::Display for BoundaryError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.problem {
            BoundaryProblem::Empty => write!(formatter, "{} must not be empty", self.field),
            BoundaryProblem::TooLarge => write!(
                formatter,
                "{} contains {} items/bytes; maximum is {}",
                self.field, self.actual, self.maximum
            ),
            BoundaryProblem::InvalidJson => write!(
                formatter,
                "{} must contain one valid JSON value within {} bytes",
                self.field, self.maximum
            ),
        }
    }
}

impl Error for BoundaryError {}

/// UTF-8 text whose allocation can never exceed `MAX` bytes.
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct BoundedText<const MAX: usize>(String);

impl<const MAX: usize> BoundedText<MAX> {
    /// Creates nonempty bounded text.
    ///
    /// # Errors
    ///
    /// Returns a typed boundary error when `value` is empty or exceeds `MAX`.
    pub fn new(field: &'static str, value: impl Into<String>) -> Result<Self, BoundaryError> {
        let value = value.into();
        if value.is_empty() {
            return Err(BoundaryError::empty(field, MAX));
        }
        if value.len() > MAX {
            return Err(BoundaryError::too_large(field, value.len(), MAX));
        }
        Ok(Self(value))
    }

    /// Creates bounded text that may be empty.
    ///
    /// # Errors
    ///
    /// Returns a typed boundary error when `value` exceeds `MAX`.
    pub fn new_allow_empty(
        field: &'static str,
        value: impl Into<String>,
    ) -> Result<Self, BoundaryError> {
        let value = value.into();
        if value.len() > MAX {
            return Err(BoundaryError::too_large(field, value.len(), MAX));
        }
        Ok(Self(value))
    }

    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    #[must_use]
    pub fn into_inner(self) -> String {
        self.0
    }
}

impl<const MAX: usize> fmt::Debug for BoundedText<MAX> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("BoundedText")
            .field("bytes", &self.0.len())
            .field("maximum", &MAX)
            .finish()
    }
}

impl<const MAX: usize> Deref for BoundedText<MAX> {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Collection whose element count can never exceed `MAX`.
#[derive(Clone, Eq, PartialEq)]
pub struct BoundedVec<T, const MAX: usize>(Vec<T>);

impl<T, const MAX: usize> BoundedVec<T, MAX> {
    #[must_use]
    pub const fn new() -> Self {
        Self(Vec::new())
    }

    /// Admits an existing vector only when its length is within the bound.
    ///
    /// # Errors
    ///
    /// Returns a typed boundary error when the vector exceeds `MAX` elements.
    pub fn try_from_vec(field: &'static str, values: Vec<T>) -> Result<Self, BoundaryError> {
        if values.len() > MAX {
            return Err(BoundaryError::too_large(field, values.len(), MAX));
        }
        Ok(Self(values))
    }

    /// Adds one item without allowing the collection to grow past `MAX`.
    ///
    /// # Errors
    ///
    /// Returns a typed boundary error when the collection is already full.
    pub fn try_push(&mut self, field: &'static str, value: T) -> Result<(), BoundaryError> {
        if self.0.len() == MAX {
            return Err(BoundaryError::too_large(field, self.0.len() + 1, MAX));
        }
        self.0.push(value);
        Ok(())
    }

    #[must_use]
    pub fn as_slice(&self) -> &[T] {
        &self.0
    }

    #[must_use]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    #[must_use]
    pub fn into_inner(self) -> Vec<T> {
        self.0
    }
}

impl<T, const MAX: usize> Default for BoundedVec<T, MAX> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T, const MAX: usize> fmt::Debug for BoundedVec<T, MAX> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("BoundedVec")
            .field("length", &self.0.len())
            .field("maximum", &MAX)
            .finish()
    }
}

/// Grid input normalized before the coordinator consumes it.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GridEvent {
    pub sequence: u64,
    pub kind: GridEventKind,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GridEventKind {
    BackendReady,
    Connected,
    Disconnected {
        reason: BoundedText<MAX_OBSERVABLE_DETAIL_BYTES>,
    },
    PublicChat {
        avatar_id: UUID,
        body: BoundedText<MAX_MESSAGE_BYTES>,
    },
    InstantMessage {
        avatar_id: UUID,
        body: BoundedText<MAX_MESSAGE_BYTES>,
    },
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MessageRole {
    System,
    Avatar,
    Agent,
    Tool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConversationMessage {
    pub role: MessageRole,
    pub body: BoundedText<MAX_MESSAGE_BYTES>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Conversation {
    pub avatar_id: UUID,
    pub messages: BoundedVec<ConversationMessage, MAX_CONVERSATION_MESSAGES>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LlmRequest {
    pub request_id: u64,
    pub conversation: Conversation,
    pub max_output_bytes: usize,
}

impl LlmRequest {
    /// Validates the request-specific output limit.
    ///
    /// # Errors
    ///
    /// Returns a boundary error when the limit is zero or above the hard body bound.
    pub fn validate(&self) -> Result<(), BoundaryError> {
        if self.max_output_bytes == 0 {
            return Err(BoundaryError::empty(
                "llm_request.max_output_bytes",
                MAX_BODY_BYTES,
            ));
        }
        if self.max_output_bytes > MAX_BODY_BYTES {
            return Err(BoundaryError::too_large(
                "llm_request.max_output_bytes",
                self.max_output_bytes,
                MAX_BODY_BYTES,
            ));
        }
        Ok(())
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProposedToolCall {
    pub call_id: BoundedText<MAX_IDENTIFIER_BYTES>,
    pub name: BoundedText<MAX_IDENTIFIER_BYTES>,
    pub arguments_json: BoundedText<MAX_TOOL_ARGUMENT_BYTES>,
}

impl ProposedToolCall {
    /// Creates a proposed call only from a bounded, syntactically valid JSON value.
    ///
    /// # Errors
    ///
    /// Returns a boundary error for invalid identifiers, size, or JSON syntax.
    pub fn new(
        call_id: impl Into<String>,
        name: impl Into<String>,
        arguments_json: impl Into<String>,
    ) -> Result<Self, BoundaryError> {
        let arguments_json = BoundedText::new("tool_call.arguments_json", arguments_json)?;
        if serde_json::from_str::<serde_json::Value>(arguments_json.as_str()).is_err() {
            return Err(BoundaryError::invalid_json(
                "tool_call.arguments_json",
                arguments_json.len(),
                MAX_TOOL_ARGUMENT_BYTES,
            ));
        }
        Ok(Self {
            call_id: BoundedText::new("tool_call.call_id", call_id)?,
            name: BoundedText::new("tool_call.name", name)?,
            arguments_json,
        })
    }

    /// Admits bounded model output before schema/JSON validation in the tool
    /// loop. This is crate-private so application-created calls still use
    /// [`Self::new`].
    pub(crate) fn from_model_output(
        call_id: impl Into<String>,
        name: impl Into<String>,
        arguments_json: impl Into<String>,
    ) -> Result<Self, BoundaryError> {
        Ok(Self {
            call_id: BoundedText::new("tool_call.call_id", call_id)?,
            name: BoundedText::new("tool_call.name", name)?,
            arguments_json: BoundedText::new("tool_call.arguments_json", arguments_json)?,
        })
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LlmResult {
    pub request_id: u64,
    pub response: BoundedText<MAX_BODY_BYTES>,
    pub proposed_calls: BoundedVec<ProposedToolCall, MAX_TOOL_CALLS>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ToolCallOutcome {
    Completed {
        call_id: BoundedText<MAX_IDENTIFIER_BYTES>,
        result: BoundedText<MAX_BODY_BYTES>,
    },
    Rejected {
        call_id: BoundedText<MAX_IDENTIFIER_BYTES>,
        reason: BoundedText<MAX_OBSERVABLE_DETAIL_BYTES>,
    },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ControlCommand {
    Pause,
    Resume,
    Shutdown,
    ReloadBehavior,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ObservableEvent {
    StateChanged {
        state: &'static str,
    },
    Grid(GridEvent),
    Policy(crate::policy::PolicyAuditRecord),
    Tool(ToolCallOutcome),
    Diagnostic {
        detail: BoundedText<MAX_OBSERVABLE_DETAIL_BYTES>,
    },
}

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

    #[test]
    fn bounded_text_and_collections_reject_overflow() {
        assert!(BoundedText::<3>::new("text", "four").is_err());
        let mut values = BoundedVec::<u8, 1>::new();
        values.try_push("values", 1).expect("first item fits");
        assert!(values.try_push("values", 2).is_err());
    }

    #[test]
    fn tool_arguments_must_be_valid_bounded_json() {
        assert!(ProposedToolCall::new("1", "look", "{\"range\": 10}").is_ok());
        assert!(ProposedToolCall::new("1", "look", "not-json").is_err());
    }
}
