//! Identity types and lightweight output DTOs for the session layer.

// ── SessionId ─────────────────────────────────────────────────────────────────

/// Unique identifier for a session.
///
/// Monotonically increasing `u64`, assigned by `TidalDb::start_session`.
/// Guaranteed unique within a process lifetime; not guaranteed across restarts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SessionId(pub(crate) u64);

impl SessionId {
    /// Wrap a raw `u64` as a `SessionId`. Used for deserialization.
    #[must_use]
    pub const fn from_raw(v: u64) -> Self {
        Self(v)
    }

    /// Return the underlying `u64`.
    #[must_use]
    pub const fn as_u64(self) -> u64 {
        self.0
    }
}

impl std::fmt::Display for SessionId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "session:{}", self.0)
    }
}

// ── AgentId ───────────────────────────────────────────────────────────────────

/// Identifier for an agent that created a session.
///
/// Must match `[a-z0-9_-]+`, max 64 characters.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AgentId(pub(crate) String);

impl AgentId {
    /// Create an `AgentId`, validating the format.
    ///
    /// # Errors
    ///
    /// Returns `Err(message)` if the string is empty, too long, or contains
    /// characters outside `[a-z0-9_-]`.
    pub fn new(s: &str) -> Result<Self, String> {
        if s.is_empty() || s.len() > 64 {
            return Err(format!("agent_id must be 1–64 chars, got len={}", s.len()));
        }
        if !s
            .bytes()
            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-')
        {
            return Err(format!("agent_id must match [a-z0-9_-], got: '{s}'"));
        }
        Ok(Self(s.to_owned()))
    }

    /// Return the agent ID as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for AgentId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

// ── SessionInfo ───────────────────────────────────────────────────────────────

/// Lightweight info about an active session. Returned by `active_sessions()`.
#[derive(Debug, Clone)]
pub struct SessionInfo {
    pub id: SessionId,
    pub user_id: u64,
    pub agent_id: String,
    pub started_at_ns: u64,
    pub signals_written: u64,
}

// ── SessionSummary ────────────────────────────────────────────────────────────

/// Summary returned by `close_session()`.
#[derive(Debug, Clone)]
pub struct SessionSummary {
    pub id: SessionId,
    pub duration_ms: u64,
    pub signals_written: u64,
    pub rejections: u64,
    /// `true` if this session was auto-closed by the TTL sweeper or shutdown,
    /// rather than explicitly closed by the agent via `close_session()`.
    pub auto_closed: bool,
}

// ── Unit tests ────────────────────────────────────────────────────────────────

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

    #[test]
    fn session_id_display() {
        let id = SessionId(42);
        assert_eq!(id.to_string(), "session:42");
    }

    #[test]
    fn session_id_roundtrip() {
        let id = SessionId(99);
        assert_eq!(id.as_u64(), 99);
        assert_eq!(SessionId::from_raw(99), id);
    }

    #[test]
    fn agent_id_valid() {
        assert!(AgentId::new("planner").is_ok());
        assert!(AgentId::new("my-agent").is_ok());
        assert!(AgentId::new("agent_01").is_ok());
    }

    #[test]
    fn agent_id_invalid_empty() {
        assert!(AgentId::new("").is_err());
    }

    #[test]
    fn agent_id_invalid_uppercase() {
        assert!(AgentId::new("MyAgent").is_err());
    }

    #[test]
    fn agent_id_too_long() {
        let s = "a".repeat(65);
        assert!(AgentId::new(&s).is_err());
    }
}
