//! Core types for the Archivist.
//!
//! This module defines the fundamental data structures used throughout the archivist,
//! including session metadata, message records, and connector information.
//!
//! All types follow these conventions:
//! - IDs: UUIDv7 for time-ordered identifiers
//! - Timestamps: RFC 3339 UTC
//! - Versioning: Every record carries a "version" field for schema evolution
//! - Metadata: Free-form JSON object reserved for caller-specific fields

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Continuation type indicating how a session relates to its parent.
///
/// Defines the relationship when a session is derived from another session.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Continuation {
    /// Session splits from parent at a specific message
    Split,
    /// Session is a compacted version of parent
    Compact,
    /// Session references parent without duplication
    Reference,
    /// Session is an edited version of parent
    Edit,
    /// Session is a subagent spawned by parent via Agent tool
    Subagent,
    /// Unknown continuation type (for forward compatibility)
    Unknown,
}

/// How complete the session data in the archive is.
///
/// Tracks whether the archivist has full message history or only
/// discovery-level metadata for this session.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SessionCompleteness {
    /// Full message history is available (or session is empty-but-known-complete).
    ///
    /// Set when:
    /// - User creates a new session (session/new)
    /// - Session history is fully loaded via session/load replay
    ///
    /// This is the default for backward compatibility: existing session.json
    /// files that lack this field will deserialize as Complete.
    #[default]
    Complete,
    /// Session was discovered via connector session/list.
    /// Only ID, title, and metadata are known. No messages in the archive.
    Discovered,
    /// Messages were loaded but may be incomplete (e.g., agent compacted history).
    /// Reserved for future refresh/sync functionality.
    Partial,
}

/// Session kind indicating the type of session storage.
///
/// Distinguishes between regular chat sessions and special meta sessions
/// that track connection events rather than messages.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SessionKind {
    /// Regular chat session with messages stored in messages.jsonl
    #[default]
    Chat,
    /// Meta session tracking ACP client connection history.
    /// Stores connection events in events.jsonl rather than messages.
    /// Linked session content is fetched on-demand, not duplicated.
    AcpConnection,
}

/// Event types for ACP meta sessions.
///
/// These events track client connection lifecycle and session navigation.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MetaEventType {
    /// Client connected to the ACP server
    ClientConnected,
    /// Client disconnected from the ACP server
    ClientDisconnected,
    /// Client opened a new session
    SessionOpened,
    /// Client switched to a different session
    SessionSwitched,
    /// Client closed a session
    SessionClosed,
}

/// A single event record in an ACP meta session.
///
/// Stored in `events.jsonl` (one per line) for meta sessions.
/// These events track connection lifecycle and session navigation,
/// NOT the actual message content of sessions.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetaEventRecord {
    /// Schema version for forward compatibility
    pub version: u32,
    /// Unique event ID (UUIDv7)
    pub event_id: Uuid,
    /// Meta session this event belongs to (scroll_id)
    pub session: Uuid,
    /// Timestamp when the event occurred
    pub ts: DateTime<Utc>,
    /// Type of event
    pub event_type: MetaEventType,
    /// Human-readable description of the event
    pub description: String,
    /// Linked session ID (for SessionOpened, SessionSwitched, SessionClosed events)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub linked_session_id: Option<Uuid>,
    /// Linked connector ID (for session-related events)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub linked_connector_id: Option<String>,
    /// Linked connector title (for display purposes)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub linked_connector_title: Option<String>,
    /// Free-form metadata for additional event data
    #[serde(default)]
    pub metadata: serde_json::Value,
}

/// Status returned when registering a connector or session.
///
/// Indicates whether the registration was accepted, aliased to an existing
/// entity, or rejected.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RegisterStatus {
    /// Registration accepted with the provided or generated UID
    Accepted,
    /// Registration accepted but UID was aliased to an existing entity
    Aliased,
    /// Registration rejected due to collision or inconsistency
    Rejected,
}

/// Session metadata stored in `session.json`.
///
/// Contains all metadata about a session including its lineage, connector
/// association, and custom metadata fields.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMetadata {
    /// Schema version for forward compatibility
    pub version: u32,
    /// Unique scroll ID for this session (UUIDv7)
    pub scroll_id: Uuid,
    /// When the session was created
    pub created_at: DateTime<Utc>,
    /// When the session was last updated
    pub updated_at: DateTime<Utc>,
    /// Optional human-readable title
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Connector that owns this session
    pub connector_uid: Uuid,
    /// Native session ID from the connector (if applicable)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub native_session_id: Option<String>,
    /// Agent ID associated with this session (if applicable)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_id: Option<String>,
    /// Parent session this was derived from (if applicable)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_scroll_id: Option<Uuid>,
    /// How this session continues from its parent (if applicable)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub continuation: Option<Continuation>,
    /// Tags for categorization
    #[serde(default)]
    pub tags: Vec<String>,
    /// Free-form metadata for caller-specific fields
    #[serde(default)]
    pub metadata: serde_json::Value,
    /// If true, this session should not appear in default session listings.
    ///
    /// Sessions with `no_update=true` exist but are hidden from the main archive list.
    /// This is useful for sessions that are:
    /// - Archived/inactive and shouldn't clutter the UI
    /// - System sessions that users don't need to see
    /// - Sessions marked for cleanup but not yet deleted
    ///
    /// Use `list_sessions_all()` or pass `include_no_update=true` to include these sessions.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub no_update: bool,
    /// Session kind (Chat or AcpConnection). Defaults to Chat.
    #[serde(default)]
    pub kind: SessionKind,
    /// ACP client ID (only for AcpConnection sessions)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub acp_client_id: Option<String>,
    /// Whether the ACP client is currently connected (only for AcpConnection sessions)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_connected: Option<bool>,
    /// Currently active linked session ID (only for AcpConnection sessions)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub current_session_id: Option<Uuid>,
    /// Agent models metadata (e.g., available models and current selection).
    /// Stored as JSON for forward compatibility with protocol changes.
    /// Contains `availableModels` and `currentModelId`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub models: Option<serde_json::Value>,
    /// Agent modes metadata (e.g., permission modes, plan mode).
    /// Stored as JSON for forward compatibility with protocol changes.
    /// Contains `availableModes` and `currentModeId`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub modes: Option<serde_json::Value>,
    /// ACP config options (JSON-serialized Vec<ConfigOption>).
    /// Stored as JSON to avoid coupling the archivist to protocol types.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub config_options: Option<serde_json::Value>,
    /// How complete the archived session data is.
    /// Defaults to Complete for backward compatibility with existing archives.
    #[serde(default)]
    pub completeness: SessionCompleteness,
    /// Matrix room ID this session is shared to (e.g. "!abc:matrix.org").
    /// Retained even when sharing is disabled so re-enabling can reconnect.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub matrix_room_id: Option<String>,
    /// Whether Matrix sharing is currently active for this session.
    #[serde(default)]
    pub matrix_sharing_active: bool,
    /// ISO 8601 timestamp of when sharing was first enabled.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub matrix_shared_at: Option<DateTime<Utc>>,
    /// Whether this session is a subagent (non-loadable, hidden from default lists).
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub is_subagent: bool,
    /// Subagent type (e.g., "Explore", "rust-task-implementer").
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub subagent_type: Option<String>,
    /// The Agent tool_use block ID in the parent that spawned this subagent.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub spawning_tool_use_id: Option<String>,
}

/// Reference to an attached file in a message.
///
/// Links to a file stored in the archive's file storage.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentRef {
    /// File identifier (e.g., "sha256:...")
    pub file_id: String,
    /// Original filename
    pub name: String,
    /// MIME type of the file (if known)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
}

/// A single message record stored in `messages.ndjson`.
///
/// Each line in the NDJSON file is one message record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageRecord {
    /// Schema version for forward compatibility
    pub version: u32,
    /// Unique message ID (UUIDv7)
    pub message_id: Uuid,
    /// Session this message belongs to (scroll_id)
    pub session: Uuid,
    /// Parent message ID (if this is a response or continuation)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_id: Option<Uuid>,
    /// Timestamp when the message was created
    pub ts: DateTime<Utc>,
    /// Message role (e.g., "system", "user", "assistant")
    pub role: String,
    /// Optional author identifier
    #[serde(skip_serializing_if = "Option::is_none")]
    pub author: Option<String>,
    /// Message content in Markdown format (for search and fallback display)
    pub content_md: String,
    /// Original content parts for rich rendering (tool calls, code blocks, etc.)
    /// This field preserves the structured MessagePart data for proper UI rendering.
    /// If None, the UI should fall back to rendering content_md as plain text.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub content_parts: Option<serde_json::Value>,
    /// Attached files
    #[serde(default)]
    pub attachments: Vec<AttachmentRef>,
    /// Free-form metadata for connector-specific fields
    #[serde(default)]
    pub metadata: serde_json::Value,
}

/// Connector metadata stored in `connector.json`.
///
/// Contains information about a connector including its type, title, and
/// native client identifier.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectorRecord {
    /// Schema version for forward compatibility
    pub version: u32,
    /// Unique connector UID (UUIDv7)
    pub connector_uid: Uuid,
    /// Connector type (e.g., "OpenCode", "ACP", "Other")
    #[serde(rename = "type")]
    pub r#type: String,
    /// Human-readable title
    pub title: String,
    /// Native client identifier (e.g., "opencode@http://localhost:12225")
    pub client_native_id: String,
    /// If this connector is an alias of another (for deduplication)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alias_of: Option<Uuid>,
    /// When the connector was registered
    pub created_at: DateTime<Utc>,
    /// Free-form metadata
    #[serde(default)]
    pub metadata: serde_json::Value,
    /// Stable fingerprint for identity matching across connector re-registrations.
    /// Format: "{transport}:{command_or_url}" e.g. "acp/stdio:/usr/bin/claude"
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fingerprint: Option<String>,
}

/// Session mapping entry stored in `sessions.ndjson`.
///
/// Maps native session IDs from connectors to scroll IDs in the archive.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMapping {
    /// Schema version for forward compatibility
    pub version: u32,
    /// Connector this mapping belongs to
    pub connector_uid: Uuid,
    /// Native session ID from the connector
    pub native_session_id: String,
    /// Scroll ID in the archive
    pub scroll_id: Uuid,
    /// When this mapping was created
    pub created_at: DateTime<Utc>,
    /// If this session mapping is an alias of another
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alias_of: Option<Uuid>,
}

/// Row in the connector index TSV file.
///
/// Note: TSV serialization is custom, not derived from serde.
#[derive(Debug, Clone)]
pub struct ConnectorIndexRow {
    /// Connector UID
    pub connector_uid: Uuid,
    /// Connector type
    pub r#type: String,
    /// Connector title
    pub title: String,
    /// Native client identifier
    pub client_native_id: String,
    /// Alias of another connector (if applicable)
    pub alias_of: Option<Uuid>,
    /// Creation timestamp
    pub created_at: DateTime<Utc>,
    /// Stable fingerprint for identity matching across connector re-registrations.
    pub fingerprint: Option<String>,
}

/// File record stored in `file_index.ndjson`.
///
/// Tracks files stored in the archive's content-addressable storage.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileRecord {
    /// Schema version for forward compatibility
    pub version: u32,
    /// File identifier (e.g., "sha256:...")
    pub file_id: String,
    /// Relative path in archive storage
    pub path: String,
    /// File size in bytes
    pub size: u64,
    /// MIME type (if known)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mime: Option<String>,
    /// Original filename
    pub original_name: String,
    /// Sessions that reference this file
    #[serde(default)]
    pub sessions: Vec<Uuid>,
    /// Free-form metadata
    #[serde(default)]
    pub metadata: serde_json::Value,
}

/// Report from a bulk session move operation.
///
/// Tracks success/failure counts and collects error messages for any
/// sessions that could not be moved.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MoveReport {
    /// Number of sessions successfully moved
    pub moved: usize,
    /// Number of sessions that failed to move
    pub failed: usize,
    /// Error messages for failed moves (one per failure)
    pub errors: Vec<String>,
}

// ============================================================================
// API Request/Response Types
// ============================================================================

/// Request to register a new connector.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterConnectorRequest {
    /// Connector type (e.g., "OpenCode", "ACP")
    #[serde(rename = "type")]
    pub r#type: String,
    /// Human-readable title
    pub title: String,
    /// Native client identifier
    pub client_native_id: String,
    /// Optional custom UID (if not provided, one will be generated)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub custom_uid: Option<Uuid>,
    /// Free-form metadata
    #[serde(default)]
    pub metadata: serde_json::Value,
    /// Stable fingerprint for identity matching across connector re-registrations.
    /// Format: "{transport}:{command_or_url}" e.g. "acp/stdio:/usr/bin/claude"
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fingerprint: Option<String>,
}

/// Response from registering a connector.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterConnectorResponse {
    /// Registration status
    pub status: RegisterStatus,
    /// Assigned or existing connector UID
    pub connector_uid: Uuid,
    /// If aliased, the UID this was aliased to
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alias_of: Option<Uuid>,
    /// Optional note explaining the result
    #[serde(skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
}

/// Request to register a new session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterSessionRequest {
    /// Connector that owns this session
    pub connector_uid: Uuid,
    /// Native session ID from the connector
    pub native_session_id: String,
    /// Optional human-readable title
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Optional custom scroll ID (if not provided, one will be generated)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub custom_scroll_id: Option<Uuid>,
    /// Free-form metadata
    #[serde(default)]
    pub metadata: serde_json::Value,
    /// Completeness level for this session. Defaults to Complete.
    #[serde(default)]
    pub completeness: SessionCompleteness,
    /// Parent session scroll ID (for subagent linkage).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_scroll_id: Option<Uuid>,
    /// Whether this is a subagent session.
    #[serde(default)]
    pub is_subagent: bool,
    /// How this session continues from parent.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub continuation: Option<Continuation>,
    /// Agent ID for this session (subagent identifier).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub agent_id: Option<String>,
    /// Subagent type (e.g., "Explore").
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub subagent_type: Option<String>,
    /// The Agent tool_use block ID in the parent that spawned this subagent.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub spawning_tool_use_id: Option<String>,
}

/// Response from registering a session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterSessionResponse {
    /// Registration status
    pub status: RegisterStatus,
    /// Assigned or existing scroll ID
    pub scroll_id: Uuid,
    /// If aliased, the scroll ID this was aliased to
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alias_of: Option<Uuid>,
}

/// An edge in the session DAG — links a parent session to a child subagent session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DagEdge {
    /// Parent session scroll_id
    pub parent: Uuid,
    /// Child (subagent) session scroll_id
    pub child: Uuid,
    /// Subagent's agent_id from Claude Code
    pub agent_id: String,
    /// Subagent type (e.g., "Explore", "rust-task-implementer")
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub subagent_type: Option<String>,
    /// The tool_use_id of the Agent call that spawned this subagent
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_use_id: Option<String>,
    /// Timestamp when the subagent was spawned
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ts: Option<DateTime<Utc>>,
}

// ---------------------------------------------------------------------------
// Session listing — cursor-paged query types
// ---------------------------------------------------------------------------

/// Maximum number of items a single `list_sessions_paged` call may return.
/// The server clamps `SessionListQuery::limit` to this value.
pub const MAX_PAGE_LIMIT: usize = 200;

/// Cursor into a sorted session listing.
///
/// Sessions are ordered by `(updated_at DESC, scroll_id DESC)`. A cursor
/// means "items strictly after this `(updated_at, scroll_id)` point in the
/// ordering".
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionCursor {
    pub updated_at: chrono::DateTime<chrono::Utc>,
    pub scroll_id: uuid::Uuid,
}

/// Query shape for [`Archivist::list_sessions_paged`].
///
/// All filters are AND-combined. `connector_uids` and `project_ids` scope the
/// search; when both are empty, every session in the archive is considered.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionListQuery {
    pub archive: Option<String>,

    // Scoping
    #[serde(default)]
    pub connector_uids: Vec<uuid::Uuid>,
    #[serde(default)]
    pub project_ids: Vec<String>,
    /// Filter by `metadata.project_path` (exact match on filesystem path).
    /// Useful for imported sessions that have a working directory but no
    /// bound `Project` entity.
    pub project_path: Option<String>,

    // Visibility
    /// `false` = hide `no_update=true` and `is_subagent=true` sessions (matches
    /// legacy `list_sessions` default). `true` = include them (matches legacy
    /// `list_sessions_all`).
    pub include_hidden: bool,

    // Filters
    /// Case-insensitive substring on `SessionMetadata.title`. Sessions with
    /// `title=None` never match when this is `Some(_)`.
    pub title_query: Option<String>,
    /// AND across tags; empty = no filter.
    #[serde(default)]
    pub tags: Vec<String>,
    /// Case-insensitive substring on `metadata.model`. Sessions without a
    /// `metadata.model` string never match when this is `Some(_)`.
    pub model_filter: Option<String>,

    // Pagination
    pub cursor: Option<SessionCursor>,
    /// Requested page size. The implementation clamps to [`MAX_PAGE_LIMIT`].
    pub limit: usize,
}

impl Default for SessionListQuery {
    fn default() -> Self {
        Self {
            archive: None,
            connector_uids: Vec::new(),
            project_ids: Vec::new(),
            project_path: None,
            include_hidden: false,
            title_query: None,
            tags: Vec::new(),
            model_filter: None,
            cursor: None,
            limit: 20,
        }
    }
}

impl SessionListQuery {
    /// Builder-style helper for tests and UI code.
    ///
    /// Adds a single connector UID to the filter. Can be called multiple
    /// times to add more connectors.
    pub fn with_connector(mut self, connector_uid: uuid::Uuid) -> Self {
        self.connector_uids.push(connector_uid);
        self
    }

    /// Adds a single project ID to the filter. Can be called multiple
    /// times to add more projects.
    pub fn with_project(mut self, project_id: impl Into<String>) -> Self {
        self.project_ids.push(project_id.into());
        self
    }

    pub fn with_project_path(mut self, path: impl Into<String>) -> Self {
        self.project_path = Some(path.into());
        self
    }

    pub fn with_archive(mut self, archive: impl Into<String>) -> Self {
        self.archive = Some(archive.into());
        self
    }

    pub fn with_limit(mut self, limit: usize) -> Self {
        self.limit = limit;
        self
    }

    pub fn with_cursor(mut self, cursor: Option<SessionCursor>) -> Self {
        self.cursor = cursor;
        self
    }

    pub fn with_title_query(mut self, q: impl Into<String>) -> Self {
        self.title_query = Some(q.into());
        self
    }

    pub fn with_include_hidden(mut self, include: bool) -> Self {
        self.include_hidden = include;
        self
    }
}

/// One page of results from [`Archivist::list_sessions_paged`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionPage {
    pub items: Vec<SessionMetadata>,
    /// `None` when the scan reached the end of the ordering.
    pub next_cursor: Option<SessionCursor>,
    /// Total number of sessions matching the query (before pagination).
    /// `None` when the backend does not compute it.
    #[serde(default)]
    pub total_count: Option<usize>,
}

// ---------------------------------------------------------------------------
// Message listing — cursor-paged query types (Phase 2)
// ---------------------------------------------------------------------------

/// Cursor into a chronologically sorted message listing.
///
/// Messages are ordered by `(ts ASC, message_id ASC)`. A cursor means
/// "items strictly after this `(ts, message_id)` point in the ordering".
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessageCursor {
    pub ts: chrono::DateTime<chrono::Utc>,
    pub message_id: uuid::Uuid,
}

/// A single page of messages returned from cursor-paged reads.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessagePage {
    pub items: Vec<MessageRecord>,
    pub next_cursor: Option<MessageCursor>,
}

#[cfg(any(test, feature = "test-utils"))]
impl SessionMetadata {
    /// Test-only: minimal valid session metadata with defaulted fields.
    pub fn stub(scroll_id: Uuid) -> Self {
        let now = chrono::Utc::now();
        Self {
            version: 1,
            scroll_id,
            created_at: now,
            updated_at: now,
            title: None,
            connector_uid: Uuid::nil(),
            native_session_id: None,
            agent_id: None,
            parent_scroll_id: None,
            continuation: None,
            tags: vec![],
            metadata: serde_json::Value::Null,
            no_update: false,
            kind: SessionKind::Chat,
            acp_client_id: None,
            is_connected: None,
            current_session_id: None,
            models: None,
            modes: None,
            config_options: None,
            completeness: SessionCompleteness::default(),
            matrix_room_id: None,
            matrix_sharing_active: false,
            matrix_shared_at: None,
            is_subagent: false,
            subagent_type: None,
            spawning_tool_use_id: None,
        }
    }
}

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

    #[test]
    fn test_continuation_serialization() {
        let continuation = Continuation::Split;
        let json = serde_json::to_string(&continuation).unwrap();
        assert_eq!(json, r#""SPLIT""#);

        let deserialized: Continuation = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, continuation);

        // Test all variants
        let variants = vec![
            (Continuation::Split, r#""SPLIT""#),
            (Continuation::Compact, r#""COMPACT""#),
            (Continuation::Reference, r#""REFERENCE""#),
            (Continuation::Edit, r#""EDIT""#),
            (Continuation::Unknown, r#""UNKNOWN""#),
        ];

        for (variant, expected) in variants {
            let json = serde_json::to_string(&variant).unwrap();
            assert_eq!(json, expected);
            let deserialized: Continuation = serde_json::from_str(&json).unwrap();
            assert_eq!(deserialized, variant);
        }
    }

    #[test]
    fn test_register_status_serialization() {
        let status = RegisterStatus::Accepted;
        let json = serde_json::to_string(&status).unwrap();
        assert_eq!(json, r#""ACCEPTED""#);

        let deserialized: RegisterStatus = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, status);

        // Test all variants
        let variants = vec![
            (RegisterStatus::Accepted, r#""ACCEPTED""#),
            (RegisterStatus::Aliased, r#""ALIASED""#),
            (RegisterStatus::Rejected, r#""REJECTED""#),
        ];

        for (variant, expected) in variants {
            let json = serde_json::to_string(&variant).unwrap();
            assert_eq!(json, expected);
            let deserialized: RegisterStatus = serde_json::from_str(&json).unwrap();
            assert_eq!(deserialized, variant);
        }
    }

    #[test]
    fn test_session_metadata_roundtrip() {
        let connector_uid = Uuid::now_v7();
        let scroll_id = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let metadata = SessionMetadata {
            version: 1,
            scroll_id,
            created_at: now,
            updated_at: now,
            title: Some("Test Session".to_string()),
            connector_uid,
            native_session_id: Some("native-123".to_string()),
            agent_id: Some("claude-3-5".to_string()),
            parent_scroll_id: None,
            continuation: None,
            tags: vec!["test".to_string(), "example".to_string()],
            metadata: serde_json::json!({
                "source": "OpenCode",
                "model": "claude-3-5-sonnet"
            }),
            no_update: false,
            kind: SessionKind::Chat,
            acp_client_id: None,
            is_connected: None,
            current_session_id: None,
            models: None,
            modes: None,
            config_options: None,
            completeness: SessionCompleteness::Complete,
            matrix_room_id: None,
            matrix_sharing_active: false,
            matrix_shared_at: None,
            is_subagent: false,
            subagent_type: None,
            spawning_tool_use_id: None,
        };

        // Serialize to JSON
        let json = serde_json::to_string_pretty(&metadata).unwrap();

        // Deserialize back
        let deserialized: SessionMetadata = serde_json::from_str(&json).unwrap();

        // Verify fields
        assert_eq!(deserialized.version, 1);
        assert_eq!(deserialized.scroll_id, scroll_id);
        assert_eq!(deserialized.connector_uid, connector_uid);
        assert_eq!(deserialized.title, Some("Test Session".to_string()));
        assert_eq!(
            deserialized.native_session_id,
            Some("native-123".to_string())
        );
        assert_eq!(deserialized.tags, vec!["test", "example"]);
    }

    #[test]
    fn test_message_record_roundtrip() {
        let message_id = Uuid::now_v7();
        let session_id = Uuid::now_v7();
        let parent_id = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let message = MessageRecord {
            version: 1,
            message_id,
            session: session_id,
            parent_id: Some(parent_id),
            ts: now,
            role: "user".to_string(),
            author: Some("alice".to_string()),
            content_md: "Hello, world!".to_string(),
            content_parts: None,
            attachments: vec![AttachmentRef {
                file_id: "sha256:abc123".to_string(),
                name: "spec.pdf".to_string(),
                mime_type: Some("application/pdf".to_string()),
            }],
            metadata: serde_json::json!({
                "connector_msg_id": "native-456"
            }),
        };

        // Serialize to JSON
        let json = serde_json::to_string(&message).unwrap();

        // Deserialize back
        let deserialized: MessageRecord = serde_json::from_str(&json).unwrap();

        // Verify fields
        assert_eq!(deserialized.version, 1);
        assert_eq!(deserialized.message_id, message_id);
        assert_eq!(deserialized.session, session_id);
        assert_eq!(deserialized.parent_id, Some(parent_id));
        assert_eq!(deserialized.role, "user");
        assert_eq!(deserialized.author, Some("alice".to_string()));
        assert_eq!(deserialized.content_md, "Hello, world!");
        assert_eq!(deserialized.attachments.len(), 1);
        assert_eq!(deserialized.attachments[0].file_id, "sha256:abc123");
    }

    #[test]
    fn test_connector_record_roundtrip() {
        let connector_uid = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let connector = ConnectorRecord {
            version: 1,
            connector_uid,
            r#type: "OpenCode".to_string(),
            title: "OpenCode Local".to_string(),
            client_native_id: "opencode@http://localhost:12225".to_string(),
            alias_of: None,
            created_at: now,
            metadata: serde_json::json!({}),
            fingerprint: None,
        };

        // Serialize to JSON
        let json = serde_json::to_string_pretty(&connector).unwrap();

        // Verify "type" field is used (not "r#type")
        assert!(json.contains(r#""type""#));
        assert!(!json.contains(r#""r#type""#));

        // Deserialize back
        let deserialized: ConnectorRecord = serde_json::from_str(&json).unwrap();

        // Verify fields
        assert_eq!(deserialized.version, 1);
        assert_eq!(deserialized.connector_uid, connector_uid);
        assert_eq!(deserialized.r#type, "OpenCode");
        assert_eq!(deserialized.title, "OpenCode Local");
        assert_eq!(
            deserialized.client_native_id,
            "opencode@http://localhost:12225"
        );
        assert_eq!(deserialized.alias_of, None);
    }

    #[test]
    fn test_session_mapping_roundtrip() {
        let connector_uid = Uuid::now_v7();
        let scroll_id = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let mapping = SessionMapping {
            version: 1,
            connector_uid,
            native_session_id: "abc123".to_string(),
            scroll_id,
            created_at: now,
            alias_of: None,
        };

        // Serialize to JSON
        let json = serde_json::to_string(&mapping).unwrap();

        // Deserialize back
        let deserialized: SessionMapping = serde_json::from_str(&json).unwrap();

        // Verify fields
        assert_eq!(deserialized.version, 1);
        assert_eq!(deserialized.connector_uid, connector_uid);
        assert_eq!(deserialized.native_session_id, "abc123");
        assert_eq!(deserialized.scroll_id, scroll_id);
    }

    #[test]
    fn test_file_record_roundtrip() {
        let session1 = Uuid::now_v7();
        let session2 = Uuid::now_v7();

        let file_record = FileRecord {
            version: 1,
            file_id: "sha256:abc123def456".to_string(),
            path: ".files/ab/cd/abc123def456".to_string(),
            size: 123456,
            mime: Some("application/pdf".to_string()),
            original_name: "spec.pdf".to_string(),
            sessions: vec![session1, session2],
            metadata: serde_json::json!({
                "source": "upload"
            }),
        };

        // Serialize to JSON
        let json = serde_json::to_string(&file_record).unwrap();

        // Deserialize back
        let deserialized: FileRecord = serde_json::from_str(&json).unwrap();

        // Verify fields
        assert_eq!(deserialized.version, 1);
        assert_eq!(deserialized.file_id, "sha256:abc123def456");
        assert_eq!(deserialized.path, ".files/ab/cd/abc123def456");
        assert_eq!(deserialized.size, 123456);
        assert_eq!(deserialized.mime, Some("application/pdf".to_string()));
        assert_eq!(deserialized.original_name, "spec.pdf");
        assert_eq!(deserialized.sessions.len(), 2);
    }

    #[test]
    fn test_uuidv7_generation() {
        // Generate several UUIDv7s
        let uuid1 = Uuid::now_v7();
        std::thread::sleep(std::time::Duration::from_millis(2));
        let uuid2 = Uuid::now_v7();
        std::thread::sleep(std::time::Duration::from_millis(2));
        let uuid3 = Uuid::now_v7();

        // Verify they're valid UUIDs
        assert_eq!(uuid1.get_version_num(), 7);
        assert_eq!(uuid2.get_version_num(), 7);
        assert_eq!(uuid3.get_version_num(), 7);

        // Verify they're in time order (UUIDv7 is time-ordered)
        // Convert to bytes for comparison
        let bytes1 = uuid1.as_bytes();
        let bytes2 = uuid2.as_bytes();
        let bytes3 = uuid3.as_bytes();

        // UUIDv7 should be sortable by bytes (time-ordered)
        assert!(bytes1 < bytes2);
        assert!(bytes2 < bytes3);
    }

    #[test]
    fn test_rfc3339_timestamps() {
        let now = DateTime::<Utc>::from(SystemTime::now());

        // Serialize to RFC 3339 format
        let json = serde_json::to_string(&now).unwrap();

        // Should be in quotes and RFC 3339 format
        assert!(json.starts_with('"'));
        assert!(json.ends_with('"'));
        assert!(json.contains('T'));
        assert!(json.contains('Z') || json.contains('+'));

        // Deserialize back
        let deserialized: DateTime<Utc> = serde_json::from_str(&json).unwrap();

        // Should be within a second of the original (allowing for microsecond precision loss)
        let diff = (now.timestamp_millis() - deserialized.timestamp_millis()).abs();
        assert!(diff < 1000, "Timestamp difference too large: {} ms", diff);
    }

    #[test]
    fn test_api_request_types() {
        // Test RegisterConnectorRequest
        let request = RegisterConnectorRequest {
            r#type: "OpenCode".to_string(),
            title: "Test Connector".to_string(),
            client_native_id: "test@localhost".to_string(),
            custom_uid: Some(Uuid::now_v7()),
            metadata: serde_json::json!({ "key": "value" }),
            fingerprint: None,
        };

        let json = serde_json::to_string(&request).unwrap();
        let deserialized: RegisterConnectorRequest = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.r#type, "OpenCode");
        assert_eq!(deserialized.title, "Test Connector");

        // Test RegisterSessionRequest
        let request = RegisterSessionRequest {
            connector_uid: Uuid::now_v7(),
            native_session_id: "native-123".to_string(),
            title: Some("Test Session".to_string()),
            custom_scroll_id: Some(Uuid::now_v7()),
            metadata: serde_json::json!({}),
            completeness: SessionCompleteness::Complete,
            parent_scroll_id: None,
            is_subagent: false,
            continuation: None,
            agent_id: None,
            subagent_type: None,
            spawning_tool_use_id: None,
        };

        let json = serde_json::to_string(&request).unwrap();
        let deserialized: RegisterSessionRequest = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.native_session_id, "native-123");
        assert_eq!(deserialized.title, Some("Test Session".to_string()));
    }

    #[test]
    fn test_api_response_types() {
        // Test RegisterConnectorResponse
        let response = RegisterConnectorResponse {
            status: RegisterStatus::Accepted,
            connector_uid: Uuid::now_v7(),
            alias_of: None,
            note: Some("Successfully registered".to_string()),
        };

        let json = serde_json::to_string(&response).unwrap();
        let deserialized: RegisterConnectorResponse = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.status, RegisterStatus::Accepted);
        assert_eq!(
            deserialized.note,
            Some("Successfully registered".to_string())
        );

        // Test RegisterSessionResponse
        let response = RegisterSessionResponse {
            status: RegisterStatus::Aliased,
            scroll_id: Uuid::now_v7(),
            alias_of: Some(Uuid::now_v7()),
        };

        let json = serde_json::to_string(&response).unwrap();
        let deserialized: RegisterSessionResponse = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.status, RegisterStatus::Aliased);
        assert!(deserialized.alias_of.is_some());
    }

    #[test]
    fn test_session_kind_serialization() {
        let kind_chat = SessionKind::Chat;
        let json = serde_json::to_string(&kind_chat).unwrap();
        assert_eq!(json, r#""CHAT""#);

        let deserialized: SessionKind = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, kind_chat);

        // Test all variants
        let variants = vec![
            (SessionKind::Chat, r#""CHAT""#),
            (SessionKind::AcpConnection, r#""ACP_CONNECTION""#),
        ];

        for (variant, expected) in variants {
            let json = serde_json::to_string(&variant).unwrap();
            assert_eq!(json, expected);
            let deserialized: SessionKind = serde_json::from_str(&json).unwrap();
            assert_eq!(deserialized, variant);
        }
    }

    #[test]
    fn test_session_kind_default() {
        let kind: SessionKind = Default::default();
        assert_eq!(kind, SessionKind::Chat);
    }

    #[test]
    fn test_meta_event_type_serialization() {
        let event_type = MetaEventType::ClientConnected;
        let json = serde_json::to_string(&event_type).unwrap();
        assert_eq!(json, r#""CLIENT_CONNECTED""#);

        let deserialized: MetaEventType = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, event_type);

        // Test all variants
        let variants = vec![
            (MetaEventType::ClientConnected, r#""CLIENT_CONNECTED""#),
            (MetaEventType::ClientDisconnected, r#""CLIENT_DISCONNECTED""#),
            (MetaEventType::SessionOpened, r#""SESSION_OPENED""#),
            (MetaEventType::SessionSwitched, r#""SESSION_SWITCHED""#),
            (MetaEventType::SessionClosed, r#""SESSION_CLOSED""#),
        ];

        for (variant, expected) in variants {
            let json = serde_json::to_string(&variant).unwrap();
            assert_eq!(json, expected);
            let deserialized: MetaEventType = serde_json::from_str(&json).unwrap();
            assert_eq!(deserialized, variant);
        }
    }

    #[test]
    fn test_meta_event_record_roundtrip() {
        let event_id = Uuid::now_v7();
        let session = Uuid::now_v7();
        let linked_session = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let event = MetaEventRecord {
            version: 1,
            event_id,
            session,
            ts: now,
            event_type: MetaEventType::SessionOpened,
            description: "Client opened session".to_string(),
            linked_session_id: Some(linked_session),
            linked_connector_id: Some("connector-123".to_string()),
            linked_connector_title: Some("Claude Session".to_string()),
            metadata: serde_json::json!({
                "client_version": "1.0.0"
            }),
        };

        // Serialize to JSON
        let json = serde_json::to_string(&event).unwrap();

        // Deserialize back
        let deserialized: MetaEventRecord = serde_json::from_str(&json).unwrap();

        // Verify fields
        assert_eq!(deserialized.version, 1);
        assert_eq!(deserialized.event_id, event_id);
        assert_eq!(deserialized.session, session);
        assert_eq!(deserialized.event_type, MetaEventType::SessionOpened);
        assert_eq!(deserialized.description, "Client opened session");
        assert_eq!(deserialized.linked_session_id, Some(linked_session));
        assert_eq!(deserialized.linked_connector_id, Some("connector-123".to_string()));
        assert_eq!(deserialized.linked_connector_title, Some("Claude Session".to_string()));
    }

    #[test]
    fn test_meta_event_record_minimal() {
        let event_id = Uuid::now_v7();
        let session = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let event = MetaEventRecord {
            version: 1,
            event_id,
            session,
            ts: now,
            event_type: MetaEventType::ClientConnected,
            description: "Client connected".to_string(),
            linked_session_id: None,
            linked_connector_id: None,
            linked_connector_title: None,
            metadata: serde_json::json!({}),
        };

        // Serialize to JSON
        let json = serde_json::to_string(&event).unwrap();

        // Verify optional fields are not serialized
        assert!(!json.contains("linked_session_id"));
        assert!(!json.contains("linked_connector_id"));
        assert!(!json.contains("linked_connector_title"));

        // Deserialize back
        let deserialized: MetaEventRecord = serde_json::from_str(&json).unwrap();

        // Verify fields
        assert_eq!(deserialized.version, 1);
        assert_eq!(deserialized.event_id, event_id);
        assert_eq!(deserialized.session, session);
        assert_eq!(deserialized.event_type, MetaEventType::ClientConnected);
        assert_eq!(deserialized.description, "Client connected");
        assert_eq!(deserialized.linked_session_id, None);
        assert_eq!(deserialized.linked_connector_id, None);
        assert_eq!(deserialized.linked_connector_title, None);
    }

    #[test]
    fn test_session_metadata_with_meta_fields() {
        let connector_uid = Uuid::now_v7();
        let scroll_id = Uuid::now_v7();
        let current_session_id = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let metadata = SessionMetadata {
            version: 1,
            scroll_id,
            created_at: now,
            updated_at: now,
            title: Some("ACP Connection".to_string()),
            connector_uid,
            native_session_id: Some("acp-meta-client-123".to_string()),
            agent_id: None,
            parent_scroll_id: None,
            continuation: None,
            tags: vec![],
            metadata: serde_json::json!({}),
            no_update: false,
            kind: SessionKind::AcpConnection,
            acp_client_id: Some("client-123".to_string()),
            is_connected: Some(true),
            current_session_id: Some(current_session_id),
            models: None,
            modes: None,
            config_options: None,
            completeness: SessionCompleteness::Complete,
            matrix_room_id: None,
            matrix_sharing_active: false,
            matrix_shared_at: None,
            is_subagent: false,
            subagent_type: None,
            spawning_tool_use_id: None,
        };

        // Serialize to JSON
        let json = serde_json::to_string_pretty(&metadata).unwrap();

        // Deserialize back
        let deserialized: SessionMetadata = serde_json::from_str(&json).unwrap();

        // Verify fields
        assert_eq!(deserialized.version, 1);
        assert_eq!(deserialized.scroll_id, scroll_id);
        assert_eq!(deserialized.kind, SessionKind::AcpConnection);
        assert_eq!(deserialized.acp_client_id, Some("client-123".to_string()));
        assert_eq!(deserialized.is_connected, Some(true));
        assert_eq!(deserialized.current_session_id, Some(current_session_id));
    }

    #[test]
    fn test_session_metadata_chat_defaults() {
        let connector_uid = Uuid::now_v7();
        let scroll_id = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        // Simulate deserializing old session metadata without new fields
        let json = serde_json::json!({
            "version": 1,
            "scroll_id": scroll_id,
            "created_at": now,
            "updated_at": now,
            "title": "Old Session",
            "connector_uid": connector_uid,
            "tags": [],
            "metadata": {}
        });

        let deserialized: SessionMetadata = serde_json::from_value(json).unwrap();

        // Verify new fields use defaults
        assert_eq!(deserialized.kind, SessionKind::Chat);
        assert_eq!(deserialized.acp_client_id, None);
        assert_eq!(deserialized.is_connected, None);
        assert_eq!(deserialized.current_session_id, None);
    }
}
