use serde::{Deserialize, Serialize};

/// Unique identifier for a vector
pub type VectorId = String;

/// Namespace identifier
pub type NamespaceId = String;

/// A vector with associated metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vector {
    pub id: VectorId,
    pub values: Vec<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    /// TTL in seconds (optional, for upsert requests)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl_seconds: Option<u64>,
    /// Unix timestamp when this vector expires (internal use)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<u64>,
}

impl Vector {
    /// Check if this vector has expired
    pub fn is_expired(&self) -> bool {
        if let Some(expires_at) = self.expires_at {
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            now >= expires_at
        } else {
            false
        }
    }

    /// Check if this vector has expired against a pre-captured timestamp.
    /// Prefer this over `is_expired()` inside loops to avoid N syscalls.
    #[inline]
    pub fn is_expired_at(&self, now_secs: u64) -> bool {
        self.expires_at.is_some_and(|exp| now_secs >= exp)
    }

    /// Calculate and set expires_at from ttl_seconds
    pub fn apply_ttl(&mut self) {
        if let Some(ttl) = self.ttl_seconds {
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            self.expires_at = Some(now + ttl);
        }
    }

    /// Get remaining TTL in seconds (None if no expiration or expired)
    pub fn remaining_ttl(&self) -> Option<u64> {
        self.expires_at.and_then(|expires_at| {
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            if now < expires_at {
                Some(expires_at - now)
            } else {
                None
            }
        })
    }
}

/// Request to upsert vectors
#[derive(Debug, Deserialize)]
pub struct UpsertRequest {
    pub vectors: Vec<Vector>,
}

/// Response from upsert operation
#[derive(Debug, Serialize, Deserialize)]
pub struct UpsertResponse {
    pub upserted_count: usize,
}

/// Column-based upsert request (Turbopuffer-inspired)
/// All arrays must have equal length. Use null for missing values.
#[derive(Debug, Deserialize)]
pub struct ColumnUpsertRequest {
    /// Array of document IDs (required)
    pub ids: Vec<VectorId>,
    /// Array of vectors (required for vector namespaces)
    pub vectors: Vec<Vec<f32>>,
    /// Additional attributes as columns (optional)
    /// Each key is an attribute name, value is array of attribute values
    #[serde(default)]
    pub attributes: std::collections::HashMap<String, Vec<serde_json::Value>>,
    /// TTL in seconds for all vectors (optional)
    #[serde(default)]
    pub ttl_seconds: Option<u64>,
    /// Expected dimension (optional, for validation)
    #[serde(default)]
    pub dimension: Option<usize>,
}

impl ColumnUpsertRequest {
    /// Convert column format to row format (Vec<Vector>)
    pub fn to_vectors(&self) -> Result<Vec<Vector>, String> {
        let count = self.ids.len();

        // Validate all arrays have same length
        if self.vectors.len() != count {
            return Err(format!(
                "vectors array length ({}) doesn't match ids array length ({})",
                self.vectors.len(),
                count
            ));
        }

        for (attr_name, attr_values) in &self.attributes {
            if attr_values.len() != count {
                return Err(format!(
                    "attribute '{}' array length ({}) doesn't match ids array length ({})",
                    attr_name,
                    attr_values.len(),
                    count
                ));
            }
        }

        // Validate vector dimensions
        // Use explicit dimension if provided, otherwise derive from first vector
        let expected_dim = if let Some(dim) = self.dimension {
            Some(dim)
        } else {
            self.vectors.first().map(|v| v.len())
        };

        if let Some(expected) = expected_dim {
            for (i, vec) in self.vectors.iter().enumerate() {
                if vec.len() != expected {
                    return Err(format!(
                        "vectors[{}] has dimension {} but expected {}",
                        i,
                        vec.len(),
                        expected
                    ));
                }
            }
        }

        // Convert to row format
        let mut vectors = Vec::with_capacity(count);
        for i in 0..count {
            // Build metadata from attributes
            let metadata = if self.attributes.is_empty() {
                None
            } else {
                let mut meta = serde_json::Map::new();
                for (attr_name, attr_values) in &self.attributes {
                    let value = &attr_values[i];
                    if !value.is_null() {
                        meta.insert(attr_name.clone(), value.clone());
                    }
                }
                if meta.is_empty() {
                    None
                } else {
                    Some(serde_json::Value::Object(meta))
                }
            };

            let mut vector = Vector {
                id: self.ids[i].clone(),
                values: self.vectors[i].clone(),
                metadata,
                ttl_seconds: self.ttl_seconds,
                expires_at: None,
            };
            vector.apply_ttl();
            vectors.push(vector);
        }

        Ok(vectors)
    }
}

/// Distance metric for vector comparison
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DistanceMetric {
    #[default]
    Cosine,
    Euclidean,
    DotProduct,
}

/// Read consistency level for queries (Turbopuffer-inspired)
///
/// Controls the trade-off between read latency and data freshness.
/// - `Strong`: Read from primary only, ensures latest data (higher latency)
/// - `Eventual`: Read from any replica, may return stale data (lower latency)
/// - `BoundedStaleness`: Allow reads from replicas within staleness threshold
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ReadConsistency {
    /// Read from primary replica only - ensures latest data
    Strong,
    /// Read from any available replica - faster but may be stale
    #[default]
    Eventual,
    /// Allow staleness up to specified milliseconds
    #[serde(rename = "bounded_staleness")]
    BoundedStaleness,
}

/// Configuration for bounded staleness reads
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct StalenessConfig {
    /// Maximum acceptable staleness in milliseconds
    #[serde(default = "default_max_staleness_ms")]
    pub max_staleness_ms: u64,
}

fn default_max_staleness_ms() -> u64 {
    5000 // 5 seconds default
}

/// Query request for vector search
#[derive(Debug, Deserialize)]
pub struct QueryRequest {
    pub vector: Vec<f32>,
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    #[serde(default)]
    pub distance_metric: DistanceMetric,
    #[serde(default = "default_true")]
    pub include_metadata: bool,
    #[serde(default)]
    pub include_vectors: bool,
    /// Optional metadata filter
    #[serde(default)]
    pub filter: Option<FilterExpression>,
    /// Cursor for pagination (from previous response's next_cursor)
    #[serde(default)]
    pub cursor: Option<String>,
    /// Read consistency level (Turbopuffer-inspired)
    /// Controls trade-off between latency and data freshness
    #[serde(default)]
    pub consistency: ReadConsistency,
    /// Staleness configuration for bounded_staleness consistency
    #[serde(default)]
    pub staleness_config: Option<StalenessConfig>,
}

fn default_top_k() -> usize {
    10
}

fn default_true() -> bool {
    true
}

/// Single search result
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchResult {
    pub id: VectorId,
    pub score: f32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vector: Option<Vec<f32>>,
}

/// Query response
#[derive(Debug, Serialize, Deserialize)]
pub struct QueryResponse {
    pub results: Vec<SearchResult>,
    /// Cursor for fetching next page of results
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next_cursor: Option<String>,
    /// Whether there are more results available
    #[serde(skip_serializing_if = "Option::is_none")]
    pub has_more: Option<bool>,
    /// Server-side search time in milliseconds
    #[serde(default)]
    pub search_time_ms: u64,
}

// ============================================================================
// Cursor-based pagination types
// ============================================================================

/// Internal cursor state for pagination
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationCursor {
    /// Last seen score for cursor-based pagination
    pub last_score: f32,
    /// Last seen ID for tie-breaking
    pub last_id: String,
}

impl PaginationCursor {
    /// Create a new pagination cursor
    pub fn new(last_score: f32, last_id: String) -> Self {
        Self {
            last_score,
            last_id,
        }
    }

    /// Encode cursor to base64 string
    pub fn encode(&self) -> String {
        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
        let json = serde_json::to_string(self).unwrap_or_default();
        URL_SAFE_NO_PAD.encode(json.as_bytes())
    }

    /// Decode cursor from base64 string
    pub fn decode(cursor: &str) -> Option<Self> {
        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
        let bytes = URL_SAFE_NO_PAD.decode(cursor).ok()?;
        let json = String::from_utf8(bytes).ok()?;
        serde_json::from_str(&json).ok()
    }
}

/// Delete request
#[derive(Debug, Deserialize)]
pub struct DeleteRequest {
    pub ids: Vec<VectorId>,
}

/// Delete response
#[derive(Debug, Serialize)]
pub struct DeleteResponse {
    pub deleted_count: usize,
}

// ============================================================================
// Batch query types
// ============================================================================

/// A single query within a batch request
#[derive(Debug, Clone, Deserialize)]
pub struct BatchQueryItem {
    /// Unique identifier for this query within the batch
    #[serde(default)]
    pub id: Option<String>,
    /// The query vector
    pub vector: Vec<f32>,
    /// Number of results to return
    #[serde(default = "default_batch_top_k")]
    pub top_k: u32,
    /// Optional filter expression
    #[serde(default)]
    pub filter: Option<FilterExpression>,
    /// Whether to include metadata in results
    #[serde(default)]
    pub include_metadata: bool,
    /// Read consistency level (Turbopuffer-inspired)
    #[serde(default)]
    pub consistency: ReadConsistency,
    /// Staleness configuration for bounded_staleness consistency
    #[serde(default)]
    pub staleness_config: Option<StalenessConfig>,
}

fn default_batch_top_k() -> u32 {
    10
}

/// Batch query request - execute multiple queries in parallel
#[derive(Debug, Deserialize)]
pub struct BatchQueryRequest {
    /// List of queries to execute
    pub queries: Vec<BatchQueryItem>,
}

/// Results for a single query within a batch
#[derive(Debug, Serialize)]
pub struct BatchQueryResult {
    /// The query identifier (if provided in request)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// Query results (empty if an error occurred)
    pub results: Vec<SearchResult>,
    /// Query execution time in milliseconds
    pub latency_ms: f64,
    /// Error message if this individual query failed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// Batch query response
#[derive(Debug, Serialize)]
pub struct BatchQueryResponse {
    /// Results for each query in the batch
    pub results: Vec<BatchQueryResult>,
    /// Total execution time in milliseconds
    pub total_latency_ms: f64,
    /// Number of queries executed
    pub query_count: usize,
}

// ============================================================================
// Multi-vector search types
// ============================================================================

/// Request for multi-vector search with positive and negative vectors
#[derive(Debug, Deserialize)]
pub struct MultiVectorSearchRequest {
    /// Positive vectors to search towards (required, at least one)
    pub positive_vectors: Vec<Vec<f32>>,
    /// Weights for positive vectors (optional, defaults to equal weights)
    #[serde(default)]
    pub positive_weights: Option<Vec<f32>>,
    /// Negative vectors to search away from (optional)
    #[serde(default)]
    pub negative_vectors: Option<Vec<Vec<f32>>>,
    /// Weights for negative vectors (optional, defaults to equal weights)
    #[serde(default)]
    pub negative_weights: Option<Vec<f32>>,
    /// Number of results to return
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    /// Distance metric to use
    #[serde(default)]
    pub distance_metric: DistanceMetric,
    /// Minimum score threshold
    #[serde(default)]
    pub score_threshold: Option<f32>,
    /// Enable MMR (Maximal Marginal Relevance) for diversity
    #[serde(default)]
    pub enable_mmr: bool,
    /// Lambda parameter for MMR (0 = max diversity, 1 = max relevance)
    #[serde(default = "default_mmr_lambda")]
    pub mmr_lambda: f32,
    /// Include metadata in results
    #[serde(default = "default_true")]
    pub include_metadata: bool,
    /// Include vectors in results
    #[serde(default)]
    pub include_vectors: bool,
    /// Optional metadata filter
    #[serde(default)]
    pub filter: Option<FilterExpression>,
    /// Read consistency level (Turbopuffer-inspired)
    #[serde(default)]
    pub consistency: ReadConsistency,
    /// Staleness configuration for bounded_staleness consistency
    #[serde(default)]
    pub staleness_config: Option<StalenessConfig>,
}

fn default_mmr_lambda() -> f32 {
    0.5
}

/// Single result from multi-vector search
#[derive(Debug, Serialize, Deserialize)]
pub struct MultiVectorSearchResult {
    pub id: VectorId,
    /// Similarity score
    pub score: f32,
    /// MMR score (if MMR enabled)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mmr_score: Option<f32>,
    /// Original rank before reranking
    #[serde(skip_serializing_if = "Option::is_none")]
    pub original_rank: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vector: Option<Vec<f32>>,
}

/// Response from multi-vector search
#[derive(Debug, Serialize, Deserialize)]
pub struct MultiVectorSearchResponse {
    pub results: Vec<MultiVectorSearchResult>,
    /// The computed query vector (weighted combination of positive - negative)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub computed_query_vector: Option<Vec<f32>>,
}

// ============================================================================
// Full-text search types
// ============================================================================

/// Request to index a document for full-text search
#[derive(Debug, Serialize, Deserialize)]
pub struct IndexDocumentRequest {
    pub id: String,
    pub text: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

/// Request to index multiple documents
#[derive(Debug, Deserialize)]
pub struct IndexDocumentsRequest {
    pub documents: Vec<IndexDocumentRequest>,
}

/// Response from indexing operation
#[derive(Debug, Serialize, Deserialize)]
pub struct IndexDocumentsResponse {
    pub indexed_count: usize,
}

/// Request to search for documents
#[derive(Debug, Deserialize)]
pub struct FullTextSearchRequest {
    pub query: String,
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    /// Optional metadata filter
    #[serde(default)]
    pub filter: Option<FilterExpression>,
}

/// Single full-text search result
#[derive(Debug, Serialize, Deserialize)]
pub struct FullTextSearchResult {
    pub id: String,
    pub score: f32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

/// Full-text search response
#[derive(Debug, Serialize, Deserialize)]
pub struct FullTextSearchResponse {
    pub results: Vec<FullTextSearchResult>,
    /// Server-side search time in milliseconds
    #[serde(default)]
    pub search_time_ms: u64,
}

/// Request to delete documents from full-text index
#[derive(Debug, Deserialize)]
pub struct DeleteDocumentsRequest {
    pub ids: Vec<String>,
}

/// Response from deleting documents
#[derive(Debug, Serialize)]
pub struct DeleteDocumentsResponse {
    pub deleted_count: usize,
}

/// Full-text index statistics
#[derive(Debug, Serialize)]
pub struct FullTextIndexStats {
    pub document_count: u32,
    pub unique_terms: usize,
    pub avg_doc_length: f32,
}

// ============================================================================
// Hybrid search types (vector + full-text)
// ============================================================================

/// Hybrid search request combining vector similarity and full-text search
#[derive(Debug, Deserialize)]
pub struct HybridSearchRequest {
    /// Query vector for similarity search. Optional — if omitted, falls back to fulltext-only BM25
    /// (equivalent to vector_weight=0.0).
    #[serde(default)]
    pub vector: Option<Vec<f32>>,
    /// Text query for full-text search
    pub text: String,
    /// Number of results to return
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    /// Weight for vector search score (0.0 to 1.0)
    /// Text search weight is (1.0 - vector_weight)
    #[serde(default = "default_vector_weight")]
    pub vector_weight: f32,
    /// Distance metric for vector search
    #[serde(default)]
    pub distance_metric: DistanceMetric,
    /// Include metadata in results
    #[serde(default = "default_true")]
    pub include_metadata: bool,
    /// Include vectors in results
    #[serde(default)]
    pub include_vectors: bool,
    /// Optional metadata filter
    #[serde(default)]
    pub filter: Option<FilterExpression>,
}

fn default_vector_weight() -> f32 {
    0.5 // Equal weight by default
}

/// Single hybrid search result
#[derive(Debug, Serialize, Deserialize)]
pub struct HybridSearchResult {
    pub id: String,
    /// Combined score
    pub score: f32,
    /// Vector similarity score (normalized 0-1)
    pub vector_score: f32,
    /// Text search BM25 score (normalized 0-1)
    pub text_score: f32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vector: Option<Vec<f32>>,
}

/// Hybrid search response
#[derive(Debug, Serialize, Deserialize)]
pub struct HybridSearchResponse {
    pub results: Vec<HybridSearchResult>,
    /// Server-side search time in milliseconds
    #[serde(default)]
    pub search_time_ms: u64,
}

// ============================================================================
// Filter types for metadata filtering
// ============================================================================

/// A filter value that can be compared against metadata fields
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum FilterValue {
    String(String),
    Number(f64),
    Integer(i64),
    Boolean(bool),
    StringArray(Vec<String>),
    NumberArray(Vec<f64>),
}

impl FilterValue {
    /// Try to get as f64 for numeric comparisons
    pub fn as_f64(&self) -> Option<f64> {
        match self {
            FilterValue::Number(n) => Some(*n),
            FilterValue::Integer(i) => Some(*i as f64),
            _ => None,
        }
    }

    /// Try to get as string
    pub fn as_str(&self) -> Option<&str> {
        match self {
            FilterValue::String(s) => Some(s.as_str()),
            _ => None,
        }
    }

    /// Try to get as bool
    pub fn as_bool(&self) -> Option<bool> {
        match self {
            FilterValue::Boolean(b) => Some(*b),
            _ => None,
        }
    }
}

/// Comparison operators for filter conditions
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum FilterCondition {
    /// Equal to
    #[serde(rename = "$eq")]
    Eq(FilterValue),
    /// Not equal to
    #[serde(rename = "$ne")]
    Ne(FilterValue),
    /// Greater than
    #[serde(rename = "$gt")]
    Gt(FilterValue),
    /// Greater than or equal to
    #[serde(rename = "$gte")]
    Gte(FilterValue),
    /// Less than
    #[serde(rename = "$lt")]
    Lt(FilterValue),
    /// Less than or equal to
    #[serde(rename = "$lte")]
    Lte(FilterValue),
    /// In array
    #[serde(rename = "$in")]
    In(Vec<FilterValue>),
    /// Not in array
    #[serde(rename = "$nin")]
    NotIn(Vec<FilterValue>),
    /// Field exists
    #[serde(rename = "$exists")]
    Exists(bool),
    // =========================================================================
    // Enhanced string operators (Turbopuffer-inspired)
    // =========================================================================
    /// Contains substring (case-sensitive)
    #[serde(rename = "$contains")]
    Contains(String),
    /// Contains substring (case-insensitive)
    #[serde(rename = "$icontains")]
    IContains(String),
    /// Starts with prefix
    #[serde(rename = "$startsWith")]
    StartsWith(String),
    /// Ends with suffix
    #[serde(rename = "$endsWith")]
    EndsWith(String),
    /// Glob pattern matching (supports * and ? wildcards)
    #[serde(rename = "$glob")]
    Glob(String),
    /// Regular expression matching
    #[serde(rename = "$regex")]
    Regex(String),
    // =========================================================================
    // Array operators
    // =========================================================================
    /// Array field contains a value (checks if a JSON array field includes the given element)
    #[serde(rename = "$arrayContains")]
    ArrayContains(FilterValue),
    /// Array field contains all specified values
    #[serde(rename = "$arrayContainsAll")]
    ArrayContainsAll(Vec<FilterValue>),
    /// Array field contains any of the specified values
    #[serde(rename = "$arrayContainsAny")]
    ArrayContainsAny(Vec<FilterValue>),
}

/// A filter expression that can be a single field condition or a logical combinator
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum FilterExpression {
    /// Logical AND of multiple expressions
    And {
        #[serde(rename = "$and")]
        conditions: Vec<FilterExpression>,
    },
    /// Logical OR of multiple expressions
    Or {
        #[serde(rename = "$or")]
        conditions: Vec<FilterExpression>,
    },
    /// Single field condition
    Field {
        #[serde(flatten)]
        field: std::collections::HashMap<String, FilterCondition>,
    },
}

// ============================================================================
// Namespace quota types
// ============================================================================

/// Quota configuration for a namespace
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct QuotaConfig {
    /// Maximum number of vectors allowed (None = unlimited)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_vectors: Option<u64>,
    /// Maximum storage size in bytes (None = unlimited)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_storage_bytes: Option<u64>,
    /// Maximum dimensions per vector (None = unlimited)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_dimensions: Option<usize>,
    /// Maximum metadata size per vector in bytes (None = unlimited)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_metadata_bytes: Option<usize>,
    /// Whether to enforce quotas (soft limit = warn only, hard = reject)
    #[serde(default)]
    pub enforcement: QuotaEnforcement,
}

/// Quota enforcement mode
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum QuotaEnforcement {
    /// No enforcement, just tracking
    None,
    /// Log warnings when quota exceeded but allow operations
    Soft,
    /// Reject operations that would exceed quota
    #[default]
    Hard,
}

/// Current quota usage for a namespace
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct QuotaUsage {
    /// Current number of vectors
    pub vector_count: u64,
    /// Current storage size in bytes (estimated)
    pub storage_bytes: u64,
    /// Average vector dimensions
    pub avg_dimensions: Option<usize>,
    /// Average metadata size in bytes
    pub avg_metadata_bytes: Option<usize>,
    /// Last updated timestamp (Unix epoch)
    pub last_updated: u64,
}

impl QuotaUsage {
    /// Create new usage with current timestamp
    pub fn new(vector_count: u64, storage_bytes: u64) -> Self {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        Self {
            vector_count,
            storage_bytes,
            avg_dimensions: None,
            avg_metadata_bytes: None,
            last_updated: now,
        }
    }

    /// Update the timestamp to now
    pub fn touch(&mut self) {
        self.last_updated = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
    }
}

/// Combined quota status showing config and current usage
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuotaStatus {
    /// Namespace name
    pub namespace: String,
    /// Quota configuration
    pub config: QuotaConfig,
    /// Current usage
    pub usage: QuotaUsage,
    /// Percentage of vector quota used (0-100, None if unlimited)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vector_usage_percent: Option<f32>,
    /// Percentage of storage quota used (0-100, None if unlimited)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub storage_usage_percent: Option<f32>,
    /// Whether any quota is exceeded
    pub is_exceeded: bool,
    /// List of exceeded quota types
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub exceeded_quotas: Vec<String>,
}

impl QuotaStatus {
    /// Create a new quota status from config and usage
    pub fn new(namespace: String, config: QuotaConfig, usage: QuotaUsage) -> Self {
        let vector_usage_percent = config
            .max_vectors
            .map(|max| (usage.vector_count as f32 / max as f32) * 100.0);

        let storage_usage_percent = config
            .max_storage_bytes
            .map(|max| (usage.storage_bytes as f32 / max as f32) * 100.0);

        let mut exceeded_quotas = Vec::new();

        if let Some(max) = config.max_vectors {
            if usage.vector_count > max {
                exceeded_quotas.push("max_vectors".to_string());
            }
        }

        if let Some(max) = config.max_storage_bytes {
            if usage.storage_bytes > max {
                exceeded_quotas.push("max_storage_bytes".to_string());
            }
        }

        let is_exceeded = !exceeded_quotas.is_empty();

        Self {
            namespace,
            config,
            usage,
            vector_usage_percent,
            storage_usage_percent,
            is_exceeded,
            exceeded_quotas,
        }
    }
}

/// Request to set quota for a namespace
#[derive(Debug, Deserialize)]
pub struct SetQuotaRequest {
    /// Quota configuration to apply
    pub config: QuotaConfig,
}

/// Response from setting quota
#[derive(Debug, Serialize)]
pub struct SetQuotaResponse {
    /// Whether the operation succeeded
    pub success: bool,
    /// Namespace name
    pub namespace: String,
    /// Applied quota configuration
    pub config: QuotaConfig,
    /// Status message
    pub message: String,
}

/// Quota check result
#[derive(Debug, Clone, Serialize)]
pub struct QuotaCheckResult {
    /// Whether the operation is allowed
    pub allowed: bool,
    /// Reason if not allowed
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    /// Current usage
    pub usage: QuotaUsage,
    /// Quota that would be exceeded
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exceeded_quota: Option<String>,
}

/// Response listing all namespace quotas
#[derive(Debug, Serialize)]
pub struct QuotaListResponse {
    /// List of quota statuses per namespace
    pub quotas: Vec<QuotaStatus>,
    /// Total number of namespaces with quotas
    pub total: u64,
    /// Default quota configuration (if set)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_config: Option<QuotaConfig>,
}

/// Response for default quota query
#[derive(Debug, Serialize)]
pub struct DefaultQuotaResponse {
    /// Default quota configuration (None if not set)
    pub config: Option<QuotaConfig>,
}

/// Request to set default quota configuration
#[derive(Debug, Deserialize)]
pub struct SetDefaultQuotaRequest {
    /// Default quota configuration (None to remove)
    pub config: Option<QuotaConfig>,
}

/// Request to check if an operation would exceed quota
#[derive(Debug, Deserialize)]
pub struct QuotaCheckRequest {
    /// Vector IDs to check (simulated vectors)
    pub vector_ids: Vec<String>,
    /// Dimension of vectors (for size estimation)
    #[serde(default)]
    pub dimensions: Option<usize>,
    /// Estimated metadata size per vector
    #[serde(default)]
    pub metadata_bytes: Option<usize>,
}

// ============================================================================
// Export API Types (Turbopuffer-inspired)
// ============================================================================

/// Request to export vectors from a namespace with pagination
#[derive(Debug, Deserialize)]
pub struct ExportRequest {
    /// Maximum number of vectors to return per page (default: 1000, max: 10000)
    #[serde(default = "default_export_top_k")]
    pub top_k: usize,
    /// Cursor for pagination - the last vector ID from previous page
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cursor: Option<String>,
    /// Whether to include vector values in the response (default: true)
    #[serde(default = "default_true")]
    pub include_vectors: bool,
    /// Whether to include metadata in the response (default: true)
    #[serde(default = "default_true")]
    pub include_metadata: bool,
}

fn default_export_top_k() -> usize {
    1000
}

impl Default for ExportRequest {
    fn default() -> Self {
        Self {
            top_k: 1000,
            cursor: None,
            include_vectors: true,
            include_metadata: true,
        }
    }
}

/// A single exported vector record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportedVector {
    /// Vector ID
    pub id: String,
    /// Vector values (optional based on include_vectors)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub values: Option<Vec<f32>>,
    /// Metadata (optional based on include_metadata)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    /// TTL in seconds if set
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl_seconds: Option<u64>,
}

impl From<&Vector> for ExportedVector {
    fn from(v: &Vector) -> Self {
        Self {
            id: v.id.clone(),
            values: Some(v.values.clone()),
            metadata: v.metadata.clone(),
            ttl_seconds: v.ttl_seconds,
        }
    }
}

/// Response from export operation
#[derive(Debug, Serialize)]
pub struct ExportResponse {
    /// Exported vectors for this page
    pub vectors: Vec<ExportedVector>,
    /// Cursor for next page (None if this is the last page)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next_cursor: Option<String>,
    /// Total vectors in namespace (for progress tracking)
    pub total_count: usize,
    /// Number of vectors returned in this page
    pub returned_count: usize,
}

// ============================================================================
// Unified Query API with rank_by (Turbopuffer-inspired)
// ============================================================================

/// Ranking function for unified query API
/// Supports vector search (ANN/kNN), full-text BM25, and attribute ordering
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RankBy {
    /// Vector search: ["vector_field", "ANN"|"kNN", [query_vector]]
    /// or simplified: ["ANN", [query_vector]] for default "vector" field
    VectorSearch {
        field: String,
        method: VectorSearchMethod,
        query_vector: Vec<f32>,
    },
    /// Full-text BM25 search: ["text_field", "BM25", "query string"]
    FullTextSearch {
        field: String,
        method: String, // Always "BM25"
        query: String,
    },
    /// Attribute ordering: ["field_name", "asc"|"desc"]
    AttributeOrder {
        field: String,
        direction: SortDirection,
    },
    /// Sum of multiple ranking functions: ["Sum", [...rankings]]
    Sum(Vec<RankBy>),
    /// Max of multiple ranking functions: ["Max", [...rankings]]
    Max(Vec<RankBy>),
    /// Product with weight: ["Product", weight, ranking]
    Product { weight: f32, ranking: Box<RankBy> },
}

/// Vector search method
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum VectorSearchMethod {
    /// Approximate Nearest Neighbor (fast, default)
    #[default]
    ANN,
    /// Exact k-Nearest Neighbor (exhaustive, requires filters)
    #[serde(rename = "kNN")]
    KNN,
}

/// Sort direction for attribute ordering
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum SortDirection {
    Asc,
    #[default]
    Desc,
}

/// Unified query request with rank_by parameter (Turbopuffer-inspired)
#[derive(Debug, Deserialize)]
pub struct UnifiedQueryRequest {
    /// How to rank documents (required unless using aggregations)
    pub rank_by: RankByInput,
    /// Number of results to return
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    /// Optional metadata filter
    #[serde(default)]
    pub filter: Option<FilterExpression>,
    /// Include metadata in results
    #[serde(default = "default_true")]
    pub include_metadata: bool,
    /// Include vectors in results
    #[serde(default)]
    pub include_vectors: bool,
    /// Distance metric for vector search (default: cosine)
    #[serde(default)]
    pub distance_metric: DistanceMetric,
}

/// Input format for rank_by that handles JSON array syntax
/// Examples:
/// - ["vector", "ANN", [0.1, 0.2, 0.3]]
/// - ["text", "BM25", "search query"]
/// - ["timestamp", "desc"]
/// - ["Sum", [["title", "BM25", "query"], ["content", "BM25", "query"]]]
/// - ["Product", 2.0, ["title", "BM25", "query"]]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(from = "serde_json::Value")]
pub struct RankByInput(pub RankBy);

impl From<serde_json::Value> for RankByInput {
    fn from(value: serde_json::Value) -> Self {
        RankByInput(parse_rank_by(&value).unwrap_or_else(|| {
            // Default fallback - shouldn't happen with valid input
            RankBy::AttributeOrder {
                field: "id".to_string(),
                direction: SortDirection::Asc,
            }
        }))
    }
}

/// Parse rank_by JSON array into RankBy enum
fn parse_rank_by(value: &serde_json::Value) -> Option<RankBy> {
    let arr = value.as_array()?;
    if arr.is_empty() {
        return None;
    }

    let first = arr.first()?.as_str()?;

    match first {
        // Combination operators
        "Sum" => {
            let rankings = arr.get(1)?.as_array()?;
            let parsed: Option<Vec<RankBy>> = rankings.iter().map(parse_rank_by).collect();
            Some(RankBy::Sum(parsed?))
        }
        "Max" => {
            let rankings = arr.get(1)?.as_array()?;
            let parsed: Option<Vec<RankBy>> = rankings.iter().map(parse_rank_by).collect();
            Some(RankBy::Max(parsed?))
        }
        "Product" => {
            let weight = arr.get(1)?.as_f64()? as f32;
            let ranking = parse_rank_by(arr.get(2)?)?;
            Some(RankBy::Product {
                weight,
                ranking: Box::new(ranking),
            })
        }
        // Vector search shorthand: ["ANN", [vector]] or ["kNN", [vector]]
        "ANN" => {
            let query_vector = parse_vector_array(arr.get(1)?)?;
            Some(RankBy::VectorSearch {
                field: "vector".to_string(),
                method: VectorSearchMethod::ANN,
                query_vector,
            })
        }
        "kNN" => {
            let query_vector = parse_vector_array(arr.get(1)?)?;
            Some(RankBy::VectorSearch {
                field: "vector".to_string(),
                method: VectorSearchMethod::KNN,
                query_vector,
            })
        }
        // Field-based operations
        field => {
            let second = arr.get(1)?;

            // Check if second element is a method string
            if let Some(method_str) = second.as_str() {
                match method_str {
                    "ANN" => {
                        let query_vector = parse_vector_array(arr.get(2)?)?;
                        Some(RankBy::VectorSearch {
                            field: field.to_string(),
                            method: VectorSearchMethod::ANN,
                            query_vector,
                        })
                    }
                    "kNN" => {
                        let query_vector = parse_vector_array(arr.get(2)?)?;
                        Some(RankBy::VectorSearch {
                            field: field.to_string(),
                            method: VectorSearchMethod::KNN,
                            query_vector,
                        })
                    }
                    "BM25" => {
                        let query = arr.get(2)?.as_str()?;
                        Some(RankBy::FullTextSearch {
                            field: field.to_string(),
                            method: "BM25".to_string(),
                            query: query.to_string(),
                        })
                    }
                    "asc" => Some(RankBy::AttributeOrder {
                        field: field.to_string(),
                        direction: SortDirection::Asc,
                    }),
                    "desc" => Some(RankBy::AttributeOrder {
                        field: field.to_string(),
                        direction: SortDirection::Desc,
                    }),
                    _ => None,
                }
            } else {
                None
            }
        }
    }
}

/// Parse a JSON value into a vector of f32
fn parse_vector_array(value: &serde_json::Value) -> Option<Vec<f32>> {
    let arr = value.as_array()?;
    arr.iter().map(|v| v.as_f64().map(|n| n as f32)).collect()
}

/// Unified query response with $dist scoring
#[derive(Debug, Serialize, Deserialize)]
pub struct UnifiedQueryResponse {
    /// Search results ordered by rank_by score
    pub results: Vec<UnifiedSearchResult>,
    /// Cursor for pagination (if more results available)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next_cursor: Option<String>,
}

/// Single result from unified query
#[derive(Debug, Serialize, Deserialize)]
pub struct UnifiedSearchResult {
    /// Vector/document ID
    pub id: String,
    /// Ranking score (distance for vector search, BM25 score for text)
    /// Named $dist for Turbopuffer compatibility
    #[serde(rename = "$dist", skip_serializing_if = "Option::is_none")]
    pub dist: Option<f32>,
    /// Metadata if requested
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    /// Vector values if requested
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vector: Option<Vec<f32>>,
}

// ============================================================================
// Aggregation types (Turbopuffer-inspired)
// ============================================================================

/// Aggregate function for computing values across documents
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AggregateFunction {
    /// Count matching documents: ["Count"]
    Count,
    /// Sum numeric attribute values: ["Sum", "attribute_name"]
    Sum { field: String },
    /// Average numeric attribute values: ["Avg", "attribute_name"]
    Avg { field: String },
    /// Minimum numeric attribute value: ["Min", "attribute_name"]
    Min { field: String },
    /// Maximum numeric attribute value: ["Max", "attribute_name"]
    Max { field: String },
}

/// Wrapper for parsing aggregate function from JSON array
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(from = "serde_json::Value")]
pub struct AggregateFunctionInput(pub AggregateFunction);

impl From<serde_json::Value> for AggregateFunctionInput {
    fn from(value: serde_json::Value) -> Self {
        parse_aggregate_function(&value)
            .map(AggregateFunctionInput)
            .unwrap_or_else(|| {
                // Default to count if parsing fails
                AggregateFunctionInput(AggregateFunction::Count)
            })
    }
}

/// Parse aggregate function from JSON array
fn parse_aggregate_function(value: &serde_json::Value) -> Option<AggregateFunction> {
    let arr = value.as_array()?;
    if arr.is_empty() {
        return None;
    }

    let func_name = arr.first()?.as_str()?;

    match func_name {
        "Count" => Some(AggregateFunction::Count),
        "Sum" => {
            let field = arr.get(1)?.as_str()?;
            Some(AggregateFunction::Sum {
                field: field.to_string(),
            })
        }
        "Avg" => {
            let field = arr.get(1)?.as_str()?;
            Some(AggregateFunction::Avg {
                field: field.to_string(),
            })
        }
        "Min" => {
            let field = arr.get(1)?.as_str()?;
            Some(AggregateFunction::Min {
                field: field.to_string(),
            })
        }
        "Max" => {
            let field = arr.get(1)?.as_str()?;
            Some(AggregateFunction::Max {
                field: field.to_string(),
            })
        }
        _ => None,
    }
}

/// Request for aggregation query (Turbopuffer-inspired)
#[derive(Debug, Deserialize)]
pub struct AggregationRequest {
    /// Named aggregations to compute
    /// Example: {"my_count": ["Count"], "total_score": ["Sum", "score"]}
    pub aggregate_by: std::collections::HashMap<String, AggregateFunctionInput>,
    /// Fields to group results by (optional)
    /// Example: ["category", "status"]
    #[serde(default)]
    pub group_by: Vec<String>,
    /// Filter to apply before aggregation
    #[serde(default)]
    pub filter: Option<FilterExpression>,
    /// Maximum number of groups to return (default: 100)
    #[serde(default = "default_agg_limit")]
    pub limit: usize,
}

fn default_agg_limit() -> usize {
    100
}

/// Response for aggregation query
#[derive(Debug, Serialize, Deserialize)]
pub struct AggregationResponse {
    /// Aggregation results (without grouping)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aggregations: Option<std::collections::HashMap<String, serde_json::Value>>,
    /// Grouped aggregation results (with group_by)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub aggregation_groups: Option<Vec<AggregationGroup>>,
}

/// Single group in aggregation results
#[derive(Debug, Serialize, Deserialize)]
pub struct AggregationGroup {
    /// Group key values (flattened into object)
    #[serde(flatten)]
    pub group_key: std::collections::HashMap<String, serde_json::Value>,
    /// Aggregation results for this group
    #[serde(flatten)]
    pub aggregations: std::collections::HashMap<String, serde_json::Value>,
}

// =============================================================================
// TEXT-BASED API TYPES (Embedded Inference)
// =============================================================================

/// A text document with metadata for text-based upsert
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextDocument {
    /// Unique identifier for this document
    pub id: VectorId,
    /// The text content to be embedded
    pub text: String,
    /// Optional metadata to store with the vector
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    /// TTL in seconds (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl_seconds: Option<u64>,
}

/// Request to upsert text documents (auto-embedded)
#[derive(Debug, Deserialize)]
pub struct TextUpsertRequest {
    /// Text documents to embed and store
    pub documents: Vec<TextDocument>,
    /// Embedding model to use (default: `minilm`).
    #[serde(default)]
    pub model: Option<EmbeddingModelType>,
}

/// Response from text upsert operation
#[derive(Debug, Serialize, Deserialize)]
pub struct TextUpsertResponse {
    /// Number of documents successfully upserted
    pub upserted_count: usize,
    /// Number of tokens processed for embedding
    pub tokens_processed: usize,
    /// Embedding model used
    pub model: EmbeddingModelType,
    /// Time taken for embedding generation (ms)
    pub embedding_time_ms: u64,
}

/// Request for text-based query (auto-embedded)
#[derive(Debug, Deserialize)]
pub struct TextQueryRequest {
    /// The query text to search for
    pub text: String,
    /// Number of results to return
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    /// Optional filter to apply
    #[serde(default)]
    pub filter: Option<FilterExpression>,
    /// Whether to include vectors in response
    #[serde(default)]
    pub include_vectors: bool,
    /// Whether to include the original text in response (if stored in metadata)
    #[serde(default = "default_true")]
    pub include_text: bool,
    /// Embedding model to use (must match upsert model; default: `minilm`).
    #[serde(default)]
    pub model: Option<EmbeddingModelType>,
}

/// Response from text-based query
#[derive(Debug, Serialize, Deserialize)]
pub struct TextQueryResponse {
    /// Search results with similarity scores
    pub results: Vec<TextSearchResult>,
    /// Embedding model used
    pub model: EmbeddingModelType,
    /// Time taken for embedding generation (ms)
    pub embedding_time_ms: u64,
    /// Time taken for search (ms)
    pub search_time_ms: u64,
}

/// Single result from text search
#[derive(Debug, Serialize, Deserialize)]
pub struct TextSearchResult {
    /// Document ID
    pub id: VectorId,
    /// Similarity score (higher is better)
    pub score: f32,
    /// Original text (if include_text=true and stored in metadata)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
    /// Associated metadata
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    /// Vector values (if include_vectors=true)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vector: Option<Vec<f32>>,
}

/// Batch text query request
#[derive(Debug, Deserialize)]
pub struct BatchTextQueryRequest {
    /// Multiple query texts
    pub queries: Vec<String>,
    /// Number of results per query
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    /// Optional filter to apply to all queries
    #[serde(default)]
    pub filter: Option<FilterExpression>,
    /// Whether to include vectors in response
    #[serde(default)]
    pub include_vectors: bool,
    /// Embedding model to use (default: `minilm`).
    #[serde(default)]
    pub model: Option<EmbeddingModelType>,
}

/// Response from batch text query
#[derive(Debug, Serialize, Deserialize)]
pub struct BatchTextQueryResponse {
    /// Results for each query
    pub results: Vec<Vec<TextSearchResult>>,
    /// Embedding model used
    pub model: EmbeddingModelType,
    /// Total time for embedding generation (ms)
    pub embedding_time_ms: u64,
    /// Total time for search (ms)
    pub search_time_ms: u64,
}

/// Available embedding models.
///
/// Replaces the previous `model: String` field — callers now supply a
/// typed enum value, eliminating runtime string-mismatch bugs.
///
/// JSON serialisation uses lowercase identifiers:
/// `"bge-large"`, `"minilm"`, `"bge-small"`, `"e5-small"`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum EmbeddingModelType {
    /// BAAI/bge-large-en-v1.5 — highest quality, 1024 dimensions (default)
    #[default]
    #[serde(rename = "bge-large")]
    BgeLarge,
    /// all-MiniLM-L6-v2 — fast and memory-efficient, 384 dimensions
    #[serde(rename = "minilm")]
    MiniLM,
    /// BAAI/bge-small-en-v1.5 — balanced quality and speed, 384 dimensions
    #[serde(rename = "bge-small")]
    BgeSmall,
    /// intfloat/e5-small-v2 — quality-focused, 384 dimensions
    #[serde(rename = "e5-small")]
    E5Small,
}

impl EmbeddingModelType {
    /// Embedding vector dimension for this model.
    pub fn dimension(&self) -> usize {
        match self {
            EmbeddingModelType::BgeLarge => 1024,
            EmbeddingModelType::MiniLM => 384,
            EmbeddingModelType::BgeSmall => 384,
            EmbeddingModelType::E5Small => 384,
        }
    }
}

impl std::fmt::Display for EmbeddingModelType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EmbeddingModelType::BgeLarge => write!(f, "bge-large"),
            EmbeddingModelType::MiniLM => write!(f, "minilm"),
            EmbeddingModelType::BgeSmall => write!(f, "bge-small"),
            EmbeddingModelType::E5Small => write!(f, "e5-small"),
        }
    }
}

// ============================================================================
// Dakera Memory Types — AI Agent Memory Platform
// ============================================================================

/// Type of memory stored by an agent
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum MemoryType {
    /// Personal experiences and events
    #[default]
    Episodic,
    /// Facts and general knowledge
    Semantic,
    /// How-to knowledge and skills
    Procedural,
    /// Short-term, temporary context
    Working,
}

impl std::fmt::Display for MemoryType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MemoryType::Episodic => write!(f, "episodic"),
            MemoryType::Semantic => write!(f, "semantic"),
            MemoryType::Procedural => write!(f, "procedural"),
            MemoryType::Working => write!(f, "working"),
        }
    }
}

/// A memory stored by an AI agent
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
    pub id: String,
    #[serde(default)]
    pub memory_type: MemoryType,
    pub content: String,
    pub agent_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    #[serde(default = "default_importance")]
    pub importance: f32,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    pub created_at: u64,
    pub last_accessed_at: u64,
    #[serde(default)]
    pub access_count: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl_seconds: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<u64>,
}

fn default_importance() -> f32 {
    0.5
}

impl Memory {
    /// Create a new memory with current timestamps
    pub fn new(id: String, content: String, agent_id: String, memory_type: MemoryType) -> Self {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        Self {
            id,
            memory_type,
            content,
            agent_id,
            session_id: None,
            importance: 0.5,
            tags: Vec::new(),
            metadata: None,
            created_at: now,
            last_accessed_at: now,
            access_count: 0,
            ttl_seconds: None,
            expires_at: None,
        }
    }

    /// Check if this memory has expired
    pub fn is_expired(&self) -> bool {
        if let Some(expires_at) = self.expires_at {
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            now >= expires_at
        } else {
            false
        }
    }

    /// Pack memory fields into metadata for Vector storage
    pub fn to_vector_metadata(&self) -> serde_json::Value {
        let mut meta = serde_json::Map::new();
        meta.insert("_dakera_type".to_string(), serde_json::json!("memory"));
        meta.insert(
            "memory_type".to_string(),
            serde_json::json!(self.memory_type),
        );
        meta.insert("content".to_string(), serde_json::json!(self.content));
        meta.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
        if let Some(ref sid) = self.session_id {
            meta.insert("session_id".to_string(), serde_json::json!(sid));
        }
        meta.insert("importance".to_string(), serde_json::json!(self.importance));
        meta.insert("tags".to_string(), serde_json::json!(self.tags));
        meta.insert("created_at".to_string(), serde_json::json!(self.created_at));
        meta.insert(
            "last_accessed_at".to_string(),
            serde_json::json!(self.last_accessed_at),
        );
        meta.insert(
            "access_count".to_string(),
            serde_json::json!(self.access_count),
        );
        if let Some(ref ttl) = self.ttl_seconds {
            meta.insert("ttl_seconds".to_string(), serde_json::json!(ttl));
        }
        if let Some(ref expires) = self.expires_at {
            meta.insert("expires_at".to_string(), serde_json::json!(expires));
        }
        if let Some(ref user_meta) = self.metadata {
            meta.insert("user_metadata".to_string(), user_meta.clone());
        }
        serde_json::Value::Object(meta)
    }

    /// Convert a Memory to a Vector (for storage layer)
    pub fn to_vector(&self, embedding: Vec<f32>) -> Vector {
        let mut v = Vector {
            id: self.id.clone(),
            values: embedding,
            metadata: Some(self.to_vector_metadata()),
            ttl_seconds: self.ttl_seconds,
            expires_at: self.expires_at,
        };
        v.apply_ttl();
        v
    }

    /// Reconstruct a Memory from a Vector's metadata
    pub fn from_vector(vector: &Vector) -> Option<Self> {
        let meta = vector.metadata.as_ref()?.as_object()?;
        let entry_type = meta.get("_dakera_type")?.as_str()?;
        if entry_type != "memory" {
            return None;
        }

        Some(Memory {
            id: vector.id.clone(),
            memory_type: serde_json::from_value(meta.get("memory_type")?.clone())
                .unwrap_or_default(),
            content: meta.get("content")?.as_str()?.to_string(),
            agent_id: meta.get("agent_id")?.as_str()?.to_string(),
            session_id: meta
                .get("session_id")
                .and_then(|v| v.as_str())
                .map(String::from),
            importance: meta
                .get("importance")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.5) as f32,
            tags: meta
                .get("tags")
                .and_then(|v| serde_json::from_value(v.clone()).ok())
                .unwrap_or_default(),
            metadata: meta.get("user_metadata").cloned(),
            created_at: meta.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0),
            last_accessed_at: meta
                .get("last_accessed_at")
                .and_then(|v| v.as_u64())
                .unwrap_or(0),
            access_count: meta
                .get("access_count")
                .and_then(|v| v.as_u64())
                .unwrap_or(0) as u32,
            ttl_seconds: vector.ttl_seconds,
            expires_at: vector.expires_at,
        })
    }
}

/// An agent session
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
    pub id: String,
    pub agent_id: String,
    pub started_at: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ended_at: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    /// Cached count of memories in this session (updated on store/forget)
    #[serde(default)]
    pub memory_count: usize,
}

impl Session {
    pub fn new(id: String, agent_id: String) -> Self {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        Self {
            id,
            agent_id,
            started_at: now,
            ended_at: None,
            summary: None,
            metadata: None,
            memory_count: 0,
        }
    }

    /// Pack session into metadata for Vector storage
    pub fn to_vector_metadata(&self) -> serde_json::Value {
        let mut meta = serde_json::Map::new();
        meta.insert("_dakera_type".to_string(), serde_json::json!("session"));
        meta.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
        meta.insert("started_at".to_string(), serde_json::json!(self.started_at));
        if let Some(ref ended) = self.ended_at {
            meta.insert("ended_at".to_string(), serde_json::json!(ended));
        }
        if let Some(ref summary) = self.summary {
            meta.insert("summary".to_string(), serde_json::json!(summary));
        }
        if let Some(ref user_meta) = self.metadata {
            meta.insert("user_metadata".to_string(), user_meta.clone());
        }
        meta.insert(
            "memory_count".to_string(),
            serde_json::json!(self.memory_count),
        );
        serde_json::Value::Object(meta)
    }

    /// Convert to a Vector for storage (use summary or agent_id as embedding source)
    pub fn to_vector(&self, embedding: Vec<f32>) -> Vector {
        Vector {
            id: self.id.clone(),
            values: embedding,
            metadata: Some(self.to_vector_metadata()),
            ttl_seconds: None,
            expires_at: None,
        }
    }

    /// Reconstruct a Session from a Vector's metadata
    pub fn from_vector(vector: &Vector) -> Option<Self> {
        let meta = vector.metadata.as_ref()?.as_object()?;
        let entry_type = meta.get("_dakera_type")?.as_str()?;
        if entry_type != "session" {
            return None;
        }

        Some(Session {
            id: vector.id.clone(),
            agent_id: meta.get("agent_id")?.as_str()?.to_string(),
            started_at: meta.get("started_at").and_then(|v| v.as_u64()).unwrap_or(0),
            ended_at: meta.get("ended_at").and_then(|v| v.as_u64()),
            summary: meta
                .get("summary")
                .and_then(|v| v.as_str())
                .map(String::from),
            metadata: meta.get("user_metadata").cloned(),
            memory_count: meta
                .get("memory_count")
                .and_then(|v| v.as_u64())
                .unwrap_or(0) as usize,
        })
    }
}

/// Strategy for importance decay
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum DecayStrategy {
    #[default]
    Exponential,
    Linear,
    StepFunction,
    /// Power-law decay: I(t) = I₀ / (1 + k·t)^α — natural for episodic memories
    PowerLaw,
    /// Logarithmic decay: I(t) = I₀ · (1 − log₂(1 + t/h)) — slow for semantic knowledge
    Logarithmic,
    /// Flat (no decay) — for procedural/skill memories
    Flat,
}

/// Configuration for importance decay
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayConfig {
    #[serde(default)]
    pub strategy: DecayStrategy,
    #[serde(default = "default_half_life_hours")]
    pub half_life_hours: f64,
    #[serde(default = "default_min_importance")]
    pub min_importance: f32,
}

fn default_half_life_hours() -> f64 {
    168.0 // 1 week
}

fn default_min_importance() -> f32 {
    0.01
}

impl Default for DecayConfig {
    fn default() -> Self {
        Self {
            strategy: DecayStrategy::default(),
            half_life_hours: default_half_life_hours(),
            min_importance: default_min_importance(),
        }
    }
}

// ============================================================================
// Dakera Memory Request/Response Types
// ============================================================================

/// Request to store a memory
#[derive(Debug, Deserialize)]
pub struct StoreMemoryRequest {
    pub content: String,
    pub agent_id: String,
    #[serde(default)]
    pub memory_type: MemoryType,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    #[serde(default = "default_importance")]
    pub importance: f32,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl_seconds: Option<u64>,
    /// Optional explicit expiry Unix timestamp (seconds).
    /// If provided, takes precedence over ttl_seconds.
    /// On expiry the memory is hard-deleted by the decay engine, bypassing
    /// importance scoring.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<u64>,
    /// Optional custom ID (auto-generated if not provided)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
}

/// Response from storing a memory
#[derive(Debug, Serialize)]
pub struct StoreMemoryResponse {
    pub memory: Memory,
    pub embedding_time_ms: u64,
}

/// CE-12: Routing mode for smart query dispatch.
///
/// Controls which retrieval backend(s) are used when recalling or searching memories.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum RoutingMode {
    /// Automatically select the best backend based on query characteristics (default).
    #[default]
    Auto,
    /// Force pure vector-similarity search (always embeds the query).
    Vector,
    /// Force pure BM25 full-text search (no embedding inference).
    Bm25,
    /// Force hybrid search: combine vector + BM25 with adaptive weighting.
    Hybrid,
}

/// CE-14: Fusion strategy for hybrid search results.
///
/// Selects how vector-similarity and BM25 scores are combined when
/// `routing=hybrid` (or auto-classified as hybrid).
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum FusionStrategy {
    /// Reciprocal Rank Fusion — Cormack et al., SIGIR 2009.
    /// Formula: score(d) = Σ 1 / (k + rank_r(d)), k=60.
    /// NOTE: RRF penalises memories that appear in only one retriever — catastrophic for
    /// temporal recall (A/B v0.11.1: 29.2% temporal vs 42.7% MinMax). Use MinMax instead.
    Rrf,
    /// Weighted min-max normalisation: combines BM25 and vector scores independently.
    /// Default since v0.11.2 — A/B benchmark shows +6.3pp overall, +13.5pp temporal vs RRF.
    #[default]
    MinMax,
}

/// Request to recall memories by semantic query
#[derive(Debug, Deserialize)]
pub struct RecallRequest {
    pub query: String,
    pub agent_id: String,
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    #[serde(default)]
    pub memory_type: Option<MemoryType>,
    #[serde(default)]
    pub session_id: Option<String>,
    #[serde(default)]
    pub tags: Option<Vec<String>>,
    #[serde(default)]
    pub min_importance: Option<f32>,
    /// Include importance-weighted re-ranking (default: true)
    #[serde(default = "default_true")]
    pub importance_weighted: bool,
    /// COG-2: traverse KG depth-1 from recalled memories and include associatively linked memories
    #[serde(default)]
    pub include_associated: bool,
    /// COG-2: max number of associated memories to return (default: 10, max: 10)
    #[serde(default)]
    pub associated_memories_cap: Option<usize>,
    /// CE-7: only include memories created at or after this ISO-8601 timestamp (e.g. "2024-01-01T00:00:00Z")
    #[serde(default)]
    pub since: Option<String>,
    /// CE-7: only include memories created at or before this ISO-8601 timestamp (e.g. "2024-12-31T23:59:59Z")
    #[serde(default)]
    pub until: Option<String>,
    /// KG-3: KG traversal depth for associative recall (1–3, default 1).
    /// Requires `include_associated: true`. Depth 1 = direct neighbours only (COG-2 behaviour).
    #[serde(default)]
    pub associated_memories_depth: Option<u8>,
    /// KG-3: minimum edge weight to traverse (0.0–1.0, default 0.0 = all edges).
    /// Requires `include_associated: true`.
    #[serde(default)]
    pub associated_memories_min_weight: Option<f32>,
    /// CE-12: retrieval routing mode.
    /// `auto` (default) classifies the query heuristically; `vector`/`bm25`/`hybrid`
    /// force a specific backend.
    #[serde(default)]
    pub routing: RoutingMode,
    /// CE-13: apply cross-encoder reranking after ANN candidate retrieval.
    /// Fetches `top_k * 3` candidates and rescores with `bge-reranker-base`.
    /// Default: `true` (improves recall precision significantly).
    #[serde(default = "default_true")]
    pub rerank: bool,
    /// CE-14: fusion strategy when routing=Hybrid.
    /// `rrf` (default) uses Reciprocal Rank Fusion; `minmax` uses weighted min-max normalization.
    #[serde(default)]
    pub fusion: FusionStrategy,
    /// CE-17: explicit vector weight for Hybrid routing (0.0–1.0).
    /// When set, overrides the adaptive heuristic from QueryClassifier.
    /// Omit to use adaptive defaults (recommended for most callers).
    #[serde(default)]
    pub vector_weight: Option<f32>,
    /// CE-23/CE-49: pseudo-relevance feedback iteration count.
    /// When `iterations >= 2`, a second pass is run with the original query augmented
    /// by entity/date terms from pass-1 results, merged via RRF (k=60).
    /// Default: 1 (single-pass). Max: 3. Ignored for pure vector routing.
    /// BM25 routing: auto-enables PRF for temporal queries (CE-35).
    /// Hybrid routing: PRF fires for temporal queries (auto) or when iterations >= 2 (CE-49).
    #[serde(default)]
    pub iterations: Option<u8>,
    /// v0.11.0 Phase 2: after main recall, fetch session-adjacent memories within ±5 min
    /// of each top result as context enrichment. Default: true.
    /// Set to false to disable neighborhood expansion (reduces latency, lower recall).
    #[serde(default = "default_true")]
    pub neighborhood: bool,
}

/// Single recall result
#[derive(Debug, Serialize, Deserialize)]
pub struct RecallResult {
    pub memory: Memory,
    pub score: f32,
    /// Score after importance-weighted re-ranking
    #[serde(skip_serializing_if = "Option::is_none")]
    pub weighted_score: Option<f32>,
    /// Always-on multi-signal smart score (vector + importance + recency + frequency)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub smart_score: Option<f32>,
    /// KG-3: traversal depth at which this memory was found (only set on associated_memories entries).
    /// 1 = direct neighbour of a primary result, 2 = two hops, 3 = three hops.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub depth: Option<u8>,
}

/// Response from recall
#[derive(Debug, Serialize)]
pub struct RecallResponse {
    pub memories: Vec<RecallResult>,
    pub query_embedding_time_ms: u64,
    pub search_time_ms: u64,
    /// COG-2: memories linked to recalled memories via KG depth-1 traversal.
    /// Only populated when `include_associated: true` in the request.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub associated_memories: Option<Vec<RecallResult>>,
}

/// Request to forget (delete) memories
#[derive(Debug, Deserialize)]
pub struct ForgetRequest {
    pub agent_id: String,
    #[serde(default)]
    pub memory_ids: Option<Vec<String>>,
    #[serde(default)]
    pub memory_type: Option<MemoryType>,
    #[serde(default)]
    pub session_id: Option<String>,
    #[serde(default)]
    pub tags: Option<Vec<String>>,
    /// Delete memories below this importance threshold
    #[serde(default)]
    pub below_importance: Option<f32>,
}

/// Response from forget
#[derive(Debug, Serialize)]
pub struct ForgetResponse {
    pub deleted_count: usize,
}

/// Request to update a memory
#[derive(Debug, Deserialize)]
pub struct UpdateMemoryRequest {
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default)]
    pub importance: Option<f32>,
    #[serde(default)]
    pub tags: Option<Vec<String>>,
    #[serde(default)]
    pub metadata: Option<serde_json::Value>,
    #[serde(default)]
    pub memory_type: Option<MemoryType>,
}

/// Request to update importance of a memory
#[derive(Debug, Deserialize)]
pub struct UpdateImportanceRequest {
    pub memory_id: String,
    pub importance: f32,
    pub agent_id: String,
}

/// Request to consolidate related memories
#[derive(Debug, Deserialize)]
pub struct ConsolidateRequest {
    pub agent_id: String,
    /// Memory IDs to consolidate (if empty, auto-detect similar memories)
    #[serde(default)]
    pub memory_ids: Option<Vec<String>>,
    /// Similarity threshold for auto-detection (default: 0.85)
    #[serde(default = "default_consolidation_threshold")]
    pub threshold: f32,
    /// Type for the consolidated memory
    #[serde(default)]
    pub target_type: Option<MemoryType>,
}

fn default_consolidation_threshold() -> f32 {
    0.85
}

/// Response from consolidation
#[derive(Debug, Serialize)]
pub struct ConsolidateResponse {
    pub consolidated_memory: Memory,
    pub source_memory_ids: Vec<String>,
    pub memories_removed: usize,
}

/// Feedback signal for active learning
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum FeedbackSignal {
    /// Boost importance (×1.15, capped at 1.0). INT-1 canonical name.
    Upvote,
    /// Penalise importance (×0.85, floor 0.0). INT-1 canonical name.
    Downvote,
    /// Mark as irrelevant — sets `decay_flag=true`, no immediate importance change.
    Flag,
    /// Backward-compatible alias for `upvote`.
    Positive,
    /// Backward-compatible alias for `downvote`.
    Negative,
}

/// One recorded feedback event stored in memory metadata (feedback_history).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedbackHistoryEntry {
    pub signal: FeedbackSignal,
    pub timestamp: u64,
    pub old_importance: f32,
    pub new_importance: f32,
}

/// Request to provide feedback on a recalled memory (legacy — body contains memory_id)
#[derive(Debug, Deserialize)]
pub struct FeedbackRequest {
    pub agent_id: String,
    pub memory_id: String,
    pub signal: FeedbackSignal,
}

/// Request for `POST /v1/memories/{id}/feedback` (INT-1 — memory_id in path)
#[derive(Debug, Deserialize)]
pub struct MemoryFeedbackRequest {
    pub agent_id: String,
    pub signal: FeedbackSignal,
}

/// Response from feedback
#[derive(Debug, Serialize)]
pub struct FeedbackResponse {
    pub memory_id: String,
    pub new_importance: f32,
    pub signal: FeedbackSignal,
}

/// Response from `GET /v1/memories/{id}/feedback`
#[derive(Debug, Serialize)]
pub struct FeedbackHistoryResponse {
    pub memory_id: String,
    pub entries: Vec<FeedbackHistoryEntry>,
}

/// Response from `GET /v1/agents/{id}/feedback/summary`
#[derive(Debug, Serialize)]
pub struct AgentFeedbackSummary {
    pub agent_id: String,
    pub upvotes: u64,
    pub downvotes: u64,
    pub flags: u64,
    pub total_feedback: u64,
    /// Weighted-average importance across all non-expired memories (0.0–1.0).
    pub health_score: f32,
}

/// Request for `PATCH /v1/memories/{id}/importance` (INT-1 — memory_id in path)
#[derive(Debug, Deserialize)]
pub struct MemoryImportancePatchRequest {
    pub agent_id: String,
    pub importance: f32,
}

/// Query params for `GET /v1/feedback/health`
#[derive(Debug, Deserialize)]
pub struct FeedbackHealthQuery {
    pub agent_id: String,
}

/// Response from `GET /v1/feedback/health`
#[derive(Debug, Serialize)]
pub struct FeedbackHealthResponse {
    pub agent_id: String,
    /// Mean importance of all non-expired memories (0.0–1.0). Higher = healthier.
    pub health_score: f32,
    pub memory_count: usize,
    pub avg_importance: f32,
}

/// Request for advanced memory search
#[derive(Debug, Deserialize)]
pub struct SearchMemoriesRequest {
    pub agent_id: String,
    #[serde(default)]
    pub query: Option<String>,
    #[serde(default)]
    pub memory_type: Option<MemoryType>,
    #[serde(default)]
    pub session_id: Option<String>,
    #[serde(default)]
    pub tags: Option<Vec<String>>,
    #[serde(default)]
    pub min_importance: Option<f32>,
    #[serde(default)]
    pub max_importance: Option<f32>,
    #[serde(default)]
    pub created_after: Option<u64>,
    #[serde(default)]
    pub created_before: Option<u64>,
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    #[serde(default)]
    pub sort_by: Option<MemorySortField>,
    /// CE-12: retrieval routing mode (auto-detected when not specified).
    #[serde(default)]
    pub routing: RoutingMode,
    /// CE-13: apply cross-encoder reranking on vector/hybrid query results.
    /// Default: `false` (search is typically used for browsing, not precision recall).
    #[serde(default)]
    pub rerank: bool,
}

/// Fields to sort memories by
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemorySortField {
    CreatedAt,
    LastAccessedAt,
    Importance,
    AccessCount,
}

/// Response from memory search
#[derive(Debug, Serialize)]
pub struct SearchMemoriesResponse {
    pub memories: Vec<RecallResult>,
    pub total_count: usize,
}

// ============================================================================
// Dakera Session Request/Response Types
// ============================================================================

/// Request to start a session
#[derive(Debug, Deserialize)]
pub struct SessionStartRequest {
    pub agent_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    /// Optional custom session ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
}

/// Response from starting a session
#[derive(Debug, Serialize)]
pub struct SessionStartResponse {
    pub session: Session,
}

/// Request to end a session
#[derive(Debug, Deserialize)]
pub struct SessionEndRequest {
    #[serde(default)]
    pub summary: Option<String>,
    /// Auto-generate summary from session memories
    #[serde(default)]
    pub auto_summarize: bool,
}

/// Response from ending a session
#[derive(Debug, Serialize)]
pub struct SessionEndResponse {
    pub session: Session,
    pub memory_count: usize,
}

/// Response listing sessions
#[derive(Debug, Serialize)]
pub struct ListSessionsResponse {
    pub sessions: Vec<Session>,
    pub total: usize,
}

/// Response for session memories
#[derive(Debug, Serialize)]
pub struct SessionMemoriesResponse {
    pub session: Session,
    pub memories: Vec<Memory>,
    /// Total number of memories in this session (before pagination)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total: Option<usize>,
}

// ============================================================================
// Dakera Agent & Knowledge Types
// ============================================================================

/// Lightweight agent summary for batch listing (uses count() not get_all)
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AgentSummary {
    pub agent_id: String,
    pub memory_count: usize,
    pub session_count: usize,
    pub active_sessions: usize,
}

/// Agent memory statistics
#[derive(Debug, Serialize)]
pub struct AgentStats {
    pub agent_id: String,
    pub total_memories: usize,
    pub memories_by_type: std::collections::HashMap<String, usize>,
    pub total_sessions: usize,
    pub active_sessions: usize,
    pub avg_importance: f32,
    pub oldest_memory_at: Option<u64>,
    pub newest_memory_at: Option<u64>,
}

/// Response from `GET /v1/agents/{agent_id}/wake-up` (DAK-1690).
///
/// Returns the highest-scored memories for an agent using a pure metadata
/// sort (`importance × recency_weight`). No embedding inference is performed,
/// making this suitable for fast agent startup context loading.
#[derive(Debug, Serialize)]
pub struct WakeUpResponse {
    pub agent_id: String,
    /// Top-N memories sorted by `importance × recency_weight` descending.
    pub memories: Vec<Memory>,
    /// Total memories available before the top_n cap was applied.
    pub total_available: usize,
}

/// Request for knowledge graph traversal
#[derive(Debug, Deserialize)]
pub struct KnowledgeGraphRequest {
    pub agent_id: String,
    pub memory_id: String,
    #[serde(default = "default_graph_depth")]
    pub depth: usize,
    #[serde(default = "default_graph_min_similarity")]
    pub min_similarity: f32,
}

fn default_graph_depth() -> usize {
    2
}

fn default_graph_min_similarity() -> f32 {
    0.7
}

/// Knowledge graph node
#[derive(Debug, Serialize)]
pub struct KnowledgeGraphNode {
    pub memory: Memory,
    pub similarity: f32,
    pub related: Vec<KnowledgeGraphEdge>,
}

/// Knowledge graph edge
#[derive(Debug, Serialize)]
pub struct KnowledgeGraphEdge {
    pub memory_id: String,
    pub similarity: f32,
    pub shared_tags: Vec<String>,
}

/// Response from knowledge graph query
#[derive(Debug, Serialize)]
pub struct KnowledgeGraphResponse {
    pub root: KnowledgeGraphNode,
    pub total_nodes: usize,
}

// ============================================================================
// Full Knowledge Graph Types (Global Network Topology)
// ============================================================================

fn default_full_graph_max_nodes() -> usize {
    200
}

fn default_full_graph_min_similarity() -> f32 {
    0.50
}

fn default_full_graph_cluster_threshold() -> f32 {
    0.60
}

fn default_full_graph_max_edges_per_node() -> usize {
    8
}

/// Request for full knowledge graph (all memories, pairwise similarity)
#[derive(Debug, Deserialize)]
pub struct FullKnowledgeGraphRequest {
    pub agent_id: String,
    #[serde(default = "default_full_graph_max_nodes")]
    pub max_nodes: usize,
    #[serde(default = "default_full_graph_min_similarity")]
    pub min_similarity: f32,
    #[serde(default = "default_full_graph_cluster_threshold")]
    pub cluster_threshold: f32,
    #[serde(default = "default_full_graph_max_edges_per_node")]
    pub max_edges_per_node: usize,
}

/// A node in the full knowledge graph
#[derive(Debug, Serialize)]
pub struct FullGraphNode {
    pub id: String,
    pub content: String,
    pub memory_type: String,
    pub importance: f32,
    pub tags: Vec<String>,
    pub created_at: Option<String>,
    pub cluster_id: usize,
    pub centrality: f32,
}

/// An edge in the full knowledge graph
#[derive(Debug, Serialize)]
pub struct FullGraphEdge {
    pub source: String,
    pub target: String,
    pub similarity: f32,
    pub shared_tags: Vec<String>,
}

/// A cluster of related memories
#[derive(Debug, Serialize)]
pub struct GraphCluster {
    pub id: usize,
    pub node_count: usize,
    pub top_tags: Vec<String>,
    pub avg_importance: f32,
}

/// Statistics about the full knowledge graph
#[derive(Debug, Serialize)]
pub struct GraphStats {
    pub total_memories: usize,
    pub included_memories: usize,
    pub total_edges: usize,
    pub cluster_count: usize,
    pub density: f32,
    pub hub_memory_id: Option<String>,
}

/// Response from full knowledge graph query
#[derive(Debug, Serialize)]
pub struct FullKnowledgeGraphResponse {
    pub nodes: Vec<FullGraphNode>,
    pub edges: Vec<FullGraphEdge>,
    pub clusters: Vec<GraphCluster>,
    pub stats: GraphStats,
}

/// Request to summarize memories
#[derive(Debug, Deserialize)]
pub struct SummarizeRequest {
    pub agent_id: String,
    pub memory_ids: Vec<String>,
    #[serde(default)]
    pub target_type: Option<MemoryType>,
}

/// Response from summarization
#[derive(Debug, Serialize)]
pub struct SummarizeResponse {
    pub summary_memory: Memory,
    pub source_count: usize,
}

/// Request to deduplicate memories
#[derive(Debug, Deserialize)]
pub struct DeduplicateRequest {
    pub agent_id: String,
    #[serde(default = "default_dedup_threshold")]
    pub threshold: f32,
    #[serde(default)]
    pub memory_type: Option<MemoryType>,
    /// Dry run — report duplicates without merging
    #[serde(default)]
    pub dry_run: bool,
}

fn default_dedup_threshold() -> f32 {
    0.92
}

/// A group of duplicate memories
#[derive(Debug, Serialize)]
pub struct DuplicateGroup {
    pub canonical_id: String,
    pub duplicate_ids: Vec<String>,
    pub avg_similarity: f32,
}

/// Response from deduplication
#[derive(Debug, Serialize)]
pub struct DeduplicateResponse {
    pub groups: Vec<DuplicateGroup>,
    pub duplicates_found: usize,
    pub duplicates_merged: usize,
}

// ============================================================================
// Cross-Agent Memory Network Types (DASH-A)
// ============================================================================

fn default_cross_agent_min_similarity() -> f32 {
    0.3
}

fn default_cross_agent_max_nodes_per_agent() -> usize {
    50
}

fn default_cross_agent_max_cross_edges() -> usize {
    200
}

/// Request for cross-agent memory network graph
#[derive(Debug, Deserialize)]
pub struct CrossAgentNetworkRequest {
    /// Specific agent IDs to include (None = all agents)
    #[serde(default)]
    pub agent_ids: Option<Vec<String>>,
    /// Minimum cosine similarity for a cross-agent edge (default 0.3)
    #[serde(default = "default_cross_agent_min_similarity")]
    pub min_similarity: f32,
    /// Maximum memories per agent to include (top N by importance, default 50)
    #[serde(default = "default_cross_agent_max_nodes_per_agent")]
    pub max_nodes_per_agent: usize,
    /// Minimum importance score for a memory to be included (default 0.0)
    #[serde(default)]
    pub min_importance: f32,
    /// Maximum cross-agent edges to return (default 200)
    #[serde(default = "default_cross_agent_max_cross_edges")]
    pub max_cross_edges: usize,
}

/// Summary info for an agent in the cross-agent network
#[derive(Debug, Serialize)]
pub struct AgentNetworkInfo {
    pub agent_id: String,
    pub memory_count: usize,
    pub avg_importance: f32,
}

/// A memory node in the cross-agent network (includes agent_id)
#[derive(Debug, Serialize)]
pub struct AgentNetworkNode {
    pub id: String,
    pub agent_id: String,
    pub content: String,
    pub importance: f32,
    pub tags: Vec<String>,
    pub memory_type: String,
    pub created_at: u64,
}

/// An edge between memories from two different agents
#[derive(Debug, Serialize)]
pub struct AgentNetworkEdge {
    pub source: String,
    pub target: String,
    pub source_agent: String,
    pub target_agent: String,
    pub similarity: f32,
}

/// Statistics for the cross-agent network
#[derive(Debug, Serialize)]
pub struct AgentNetworkStats {
    pub total_agents: usize,
    pub total_nodes: usize,
    pub total_cross_edges: usize,
    pub density: f32,
}

/// Response from cross-agent network query
#[derive(Debug, Serialize)]
pub struct CrossAgentNetworkResponse {
    pub node_count: usize,
    pub agents: Vec<AgentNetworkInfo>,
    pub nodes: Vec<AgentNetworkNode>,
    pub edges: Vec<AgentNetworkEdge>,
    pub stats: AgentNetworkStats,
}

// ---------------------------------------------------------------------------
// CE-2: Batch recall / forget types
// ---------------------------------------------------------------------------

/// Filter predicates for batch memory operations.
///
/// At least one field must be set for forget operations (safety guard).
#[derive(Debug, Deserialize, Default)]
pub struct BatchMemoryFilter {
    /// Restrict to memories that carry **all** listed tags.
    #[serde(default)]
    pub tags: Option<Vec<String>>,
    /// Minimum importance (inclusive).
    #[serde(default)]
    pub min_importance: Option<f32>,
    /// Maximum importance (inclusive).
    #[serde(default)]
    pub max_importance: Option<f32>,
    /// Only memories created at or after this Unix timestamp (seconds).
    #[serde(default)]
    pub created_after: Option<u64>,
    /// Only memories created before or at this Unix timestamp (seconds).
    #[serde(default)]
    pub created_before: Option<u64>,
    /// Restrict to a specific memory type.
    #[serde(default)]
    pub memory_type: Option<MemoryType>,
    /// Restrict to memories from a specific session.
    #[serde(default)]
    pub session_id: Option<String>,
}

impl BatchMemoryFilter {
    /// Returns `true` if the filter has at least one constraint set.
    pub fn has_any(&self) -> bool {
        self.tags.is_some()
            || self.min_importance.is_some()
            || self.max_importance.is_some()
            || self.created_after.is_some()
            || self.created_before.is_some()
            || self.memory_type.is_some()
            || self.session_id.is_some()
    }

    /// Returns `true` if the given memory matches all active filter predicates.
    pub fn matches(&self, memory: &Memory) -> bool {
        if let Some(ref tags) = self.tags {
            if !tags.is_empty() && !tags.iter().all(|t| memory.tags.contains(t)) {
                return false;
            }
        }
        if let Some(min) = self.min_importance {
            if memory.importance < min {
                return false;
            }
        }
        if let Some(max) = self.max_importance {
            if memory.importance > max {
                return false;
            }
        }
        if let Some(after) = self.created_after {
            if memory.created_at < after {
                return false;
            }
        }
        if let Some(before) = self.created_before {
            if memory.created_at > before {
                return false;
            }
        }
        if let Some(ref mt) = self.memory_type {
            if memory.memory_type != *mt {
                return false;
            }
        }
        if let Some(ref sid) = self.session_id {
            if memory.session_id.as_ref() != Some(sid) {
                return false;
            }
        }
        true
    }
}

/// Request for `POST /v1/memories/recall/batch`
#[derive(Debug, Deserialize)]
pub struct BatchRecallRequest {
    /// Agent whose memory namespace to search.
    pub agent_id: String,
    /// Filter predicates to apply.
    #[serde(default)]
    pub filter: BatchMemoryFilter,
    /// Maximum number of results to return (default: 100).
    #[serde(default = "default_batch_limit")]
    pub limit: usize,
}

fn default_batch_limit() -> usize {
    100
}

/// Response from `POST /v1/memories/recall/batch`
#[derive(Debug, Serialize)]
pub struct BatchRecallResponse {
    pub memories: Vec<Memory>,
    pub total: usize,
    pub filtered: usize,
}

/// Request for `DELETE /v1/memories/forget/batch`
#[derive(Debug, Deserialize)]
pub struct BatchForgetRequest {
    /// Agent whose memory namespace to purge from.
    pub agent_id: String,
    /// Filter predicates — **at least one must be set** (safety guard).
    pub filter: BatchMemoryFilter,
}

/// Response from `DELETE /v1/memories/forget/batch`
#[derive(Debug, Serialize)]
pub struct BatchForgetResponse {
    pub deleted_count: usize,
}

// ─────────────────────────────────────────────────────────────────────────────
// Batch store types
// ─────────────────────────────────────────────────────────────────────────────

/// A single memory entry within a `BatchStoreMemoryRequest`.
///
/// Mirrors `StoreMemoryRequest` but omits `agent_id` (supplied at the batch level).
#[derive(Debug, Deserialize)]
pub struct BatchStoreMemoryItem {
    pub content: String,
    #[serde(default)]
    pub memory_type: MemoryType,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    #[serde(default = "default_importance")]
    pub importance: f32,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl_seconds: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<u64>,
    /// Optional custom ID. Auto-generated (unique within the batch) if not provided.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
}

/// Request for `POST /v1/memories/store/batch`
///
/// Accepts up to 1000 memories per call. All memories are embedded in a single
/// ONNX inference call and upserted in one storage write, with HNSW invalidation
/// happening exactly once at the end — yielding ≥5× throughput vs. N sequential
/// single-store calls.
#[derive(Debug, Deserialize)]
pub struct BatchStoreMemoryRequest {
    pub agent_id: String,
    pub memories: Vec<BatchStoreMemoryItem>,
}

/// Response from `POST /v1/memories/store/batch`
#[derive(Debug, Serialize)]
pub struct BatchStoreMemoryResponse {
    pub stored: Vec<Memory>,
    pub stored_count: usize,
    pub total_embedding_time_ms: u64,
}

// ─────────────────────────────────────────────────────────────────────────────
// CE-4 — Entity extraction types
// ─────────────────────────────────────────────────────────────────────────────

/// Request to update entity extraction config for a namespace.
/// `PATCH /v1/namespaces/{namespace}/config`
#[derive(Debug, Deserialize)]
pub struct NamespaceEntityConfigRequest {
    /// Enable or disable entity extraction for this namespace.
    pub extract_entities: bool,
    /// Entity types to extract via GLiNER (e.g. ["person","org","location"]).
    /// If empty and extract_entities=true, only the rule-based pre-pass runs.
    #[serde(default)]
    pub entity_types: Vec<String>,
}

/// Response from `PATCH /v1/namespaces/{namespace}/config`
#[derive(Debug, Serialize, Deserialize)]
pub struct NamespaceEntityConfigResponse {
    pub namespace: String,
    pub extract_entities: bool,
    pub entity_types: Vec<String>,
}

/// Request to extract entities from content without storing.
/// `POST /v1/memories/extract`
#[derive(Debug, Deserialize)]
pub struct ExtractEntitiesRequest {
    /// Text content to extract entities from.
    pub content: String,
    /// Entity types for GLiNER inference (optional).
    /// If omitted, only the rule-based pre-pass runs.
    #[serde(default)]
    pub entity_types: Vec<String>,
}

/// A single extracted entity (shared with inference crate — mirrored here for API types).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityResult {
    pub entity_type: String,
    pub value: String,
    pub score: f32,
    pub start: usize,
    pub end: usize,
    /// Canonical tag form: `entity:<type>:<value>`
    pub tag: String,
}

/// Response from `POST /v1/memories/extract` and `GET /v1/memories/{id}/entities`
#[derive(Debug, Serialize)]
pub struct ExtractEntitiesResponse {
    pub entities: Vec<EntityResult>,
    pub count: usize,
}

// ============================================================================
// CE-5: Memory Knowledge Graph — request / response types
// ============================================================================

/// GET /v1/memories/:id/graph
#[derive(Debug, Deserialize)]
pub struct GraphTraverseQuery {
    /// BFS depth limit (default 3, max 5).
    #[serde(default = "default_ce5_graph_depth")]
    pub depth: u32,
}

fn default_ce5_graph_depth() -> u32 {
    3
}

/// GET /v1/memories/:id/path
#[derive(Debug, Deserialize)]
pub struct GraphPathQuery {
    /// Target memory ID.
    pub to: String,
}

/// POST /v1/memories/:id/links — create an explicit edge
#[derive(Debug, Deserialize)]
pub struct MemoryLinkRequest {
    /// The other memory ID to link to.
    pub target_id: String,
    /// Optional human-readable label (stored as `linked_by` edge).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    /// Agent ID (for authorization).
    pub agent_id: String,
}

/// Response from graph traversal.
#[derive(Debug, Serialize)]
pub struct GraphTraverseResponse {
    pub root_id: String,
    pub depth: u32,
    pub node_count: usize,
    pub nodes: Vec<GraphNodeResponse>,
}

/// A single node in a graph traversal response.
#[derive(Debug, Serialize)]
pub struct GraphNodeResponse {
    pub memory_id: String,
    pub depth: u32,
    pub edges: Vec<GraphEdgeResponse>,
}

/// A single edge in a graph response.
#[derive(Debug, Serialize)]
pub struct GraphEdgeResponse {
    pub from_id: String,
    pub to_id: String,
    pub edge_type: String,
    pub weight: f32,
    pub created_at: u64,
}

/// Response from shortest-path query.
#[derive(Debug, Serialize)]
pub struct GraphPathResponse {
    pub from_id: String,
    pub to_id: String,
    /// Ordered list of memory IDs along the shortest path (inclusive).
    pub path: Vec<String>,
    pub hop_count: usize,
}

/// Response from explicit link creation.
#[derive(Debug, Serialize)]
pub struct MemoryLinkResponse {
    pub from_id: String,
    pub to_id: String,
    pub edge_type: String,
}

/// Response from agent graph export.
#[derive(Debug, Serialize)]
pub struct GraphExportResponse {
    pub agent_id: String,
    pub namespace: String,
    pub node_count: usize,
    pub edge_count: usize,
    pub edges: Vec<GraphEdgeResponse>,
}

// ============================================================================
// KG-2: Graph Query & Export — request / response types
// ============================================================================

/// GET /v1/knowledge/query — JSON DSL for graph filtering/traversal
#[derive(Debug, Deserialize)]
pub struct KgQueryParams {
    /// Agent ID whose graph to query (required).
    pub agent_id: String,
    /// Optional root memory ID — if set, performs BFS from this node first.
    #[serde(default)]
    pub root_id: Option<String>,
    /// Filter edges by type (comma-separated, e.g. "related_to,shares_entity").
    #[serde(default)]
    pub edge_type: Option<String>,
    /// Minimum edge weight (0.0–1.0).
    #[serde(default)]
    pub min_weight: Option<f32>,
    /// BFS depth when root_id is set (1–5, default 3).
    #[serde(default = "default_kg_depth")]
    pub max_depth: u32,
    /// Maximum number of edges to return (default 100, max 1000).
    #[serde(default = "default_kg_limit")]
    pub limit: usize,
}

fn default_kg_depth() -> u32 {
    3
}

fn default_kg_limit() -> usize {
    100
}

/// Response from GET /v1/knowledge/query
#[derive(Debug, Serialize)]
pub struct KgQueryResponse {
    pub agent_id: String,
    pub node_count: usize,
    pub edge_count: usize,
    pub edges: Vec<GraphEdgeResponse>,
}

/// GET /v1/knowledge/path — shortest path between two memory IDs
#[derive(Debug, Deserialize)]
pub struct KgPathParams {
    /// Agent ID for authorization.
    pub agent_id: String,
    /// Source memory ID.
    pub from: String,
    /// Target memory ID.
    pub to: String,
}

/// Response from GET /v1/knowledge/path
#[derive(Debug, Serialize)]
pub struct KgPathResponse {
    pub agent_id: String,
    pub from_id: String,
    pub to_id: String,
    pub hop_count: usize,
    pub path: Vec<String>,
}

/// GET /v1/knowledge/export — export graph as JSON or GraphML
#[derive(Debug, Deserialize)]
pub struct KgExportParams {
    /// Agent ID whose graph to export.
    pub agent_id: String,
    /// Export format: "json" (default) or "graphml".
    #[serde(default = "default_kg_format")]
    pub format: String,
}

fn default_kg_format() -> String {
    "json".to_string()
}

/// Response from GET /v1/knowledge/export (format=json)
#[derive(Debug, Serialize)]
pub struct KgExportJsonResponse {
    pub agent_id: String,
    pub format: String,
    pub node_count: usize,
    pub edge_count: usize,
    pub edges: Vec<GraphEdgeResponse>,
}

// ============================================================================
// COG-1: Cognitive Memory Lifecycle — per-namespace memory policy
// ============================================================================

fn default_working_ttl() -> Option<u64> {
    Some(14_400) // 4 hours
}
fn default_episodic_ttl() -> Option<u64> {
    Some(2_592_000) // 30 days
}
fn default_semantic_ttl() -> Option<u64> {
    Some(31_536_000) // 365 days
}
fn default_procedural_ttl() -> Option<u64> {
    Some(63_072_000) // 730 days
}
fn default_working_decay() -> DecayStrategy {
    DecayStrategy::Exponential
}
fn default_episodic_decay() -> DecayStrategy {
    DecayStrategy::PowerLaw
}
fn default_semantic_decay() -> DecayStrategy {
    DecayStrategy::Logarithmic
}
fn default_procedural_decay() -> DecayStrategy {
    DecayStrategy::Flat
}
fn default_sr_factor() -> f64 {
    1.0
}
fn default_sr_base_interval() -> u64 {
    86_400 // 1 day
}
fn default_consolidation_enabled() -> bool {
    false
}
fn default_policy_consolidation_threshold() -> f32 {
    0.92
}
fn default_consolidation_interval_hours() -> u32 {
    24
}
fn default_store_dedup_threshold() -> f32 {
    0.95
}

/// Per-namespace memory lifecycle policy (COG-1).
///
/// Controls type-specific TTLs, decay curves, and spaced repetition behaviour.
/// All fields have sensible defaults; only override what you need.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryPolicy {
    // ── Differential TTLs ────────────────────────────────────────────────────
    /// Default TTL for `working` memories in seconds (default: 4 h = 14 400 s).
    #[serde(
        default = "default_working_ttl",
        skip_serializing_if = "Option::is_none"
    )]
    pub working_ttl_seconds: Option<u64>,
    /// Default TTL for `episodic` memories in seconds (default: 30 d = 2 592 000 s).
    #[serde(
        default = "default_episodic_ttl",
        skip_serializing_if = "Option::is_none"
    )]
    pub episodic_ttl_seconds: Option<u64>,
    /// Default TTL for `semantic` memories in seconds (default: 365 d = 31 536 000 s).
    #[serde(
        default = "default_semantic_ttl",
        skip_serializing_if = "Option::is_none"
    )]
    pub semantic_ttl_seconds: Option<u64>,
    /// Default TTL for `procedural` memories in seconds (default: 730 d = 63 072 000 s).
    #[serde(
        default = "default_procedural_ttl",
        skip_serializing_if = "Option::is_none"
    )]
    pub procedural_ttl_seconds: Option<u64>,

    // ── Decay curves ─────────────────────────────────────────────────────────
    /// Decay strategy for `working` memories (default: exponential).
    #[serde(default = "default_working_decay")]
    pub working_decay: DecayStrategy,
    /// Decay strategy for `episodic` memories (default: power_law).
    #[serde(default = "default_episodic_decay")]
    pub episodic_decay: DecayStrategy,
    /// Decay strategy for `semantic` memories (default: logarithmic).
    #[serde(default = "default_semantic_decay")]
    pub semantic_decay: DecayStrategy,
    /// Decay strategy for `procedural` memories (default: flat — no decay).
    #[serde(default = "default_procedural_decay")]
    pub procedural_decay: DecayStrategy,

    // ── Spaced repetition ────────────────────────────────────────────────────
    /// Multiplier applied to the TTL extension on each recall.
    /// Extension = `access_count × sr_factor × sr_base_interval_seconds`.
    /// Set to 0.0 to disable spaced repetition. (default: 1.0)
    #[serde(default = "default_sr_factor")]
    pub spaced_repetition_factor: f64,
    /// Base interval in seconds for spaced repetition TTL extension (default: 86 400 = 1 day).
    #[serde(default = "default_sr_base_interval")]
    pub spaced_repetition_base_interval_seconds: u64,

    // ── COG-3: Proactive consolidation ───────────────────────────────────────
    /// Enable background deduplication of semantically similar memories (default: false).
    #[serde(default = "default_consolidation_enabled")]
    pub consolidation_enabled: bool,
    /// Cosine-similarity threshold for merging memories (default: 0.92, range 0.85–0.99).
    #[serde(default = "default_policy_consolidation_threshold")]
    pub consolidation_threshold: f32,
    /// How often the background consolidation job runs, in hours (default: 24).
    #[serde(default = "default_consolidation_interval_hours")]
    pub consolidation_interval_hours: u32,
    /// Total number of memories merged since namespace creation (read-only).
    #[serde(default)]
    pub consolidated_count: u64,

    // ── SEC-5: Per-namespace rate limiting ───────────────────────────────────
    /// Master rate-limit switch (default: false — opt-in to avoid breaking existing clients).
    /// Set to `true` to enforce `rate_limit_stores_per_minute` / `rate_limit_recalls_per_minute`.
    #[serde(default)]
    pub rate_limit_enabled: bool,
    /// Maximum `POST /v1/memory/store` operations per minute for this namespace.
    /// `None` = unlimited. Only enforced when `rate_limit_enabled = true`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rate_limit_stores_per_minute: Option<u32>,
    /// Maximum `POST /v1/memory/recall` operations per minute for this namespace.
    /// `None` = unlimited. Only enforced when `rate_limit_enabled = true`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rate_limit_recalls_per_minute: Option<u32>,

    // ── CE-10a: Store-time deduplication ─────────────────────────────────────
    /// Enable near-duplicate detection on every `store` call (default: false).
    ///
    /// When enabled, a quick vector-search (top-1) runs after embedding; if the
    /// nearest neighbour has cosine similarity ≥ 0.95 the new store is rejected
    /// and the existing memory ID is returned instead.  Adds one ANN query to
    /// every store operation — keep disabled for high-throughput namespaces.
    #[serde(default)]
    pub dedup_on_store: bool,
    /// Similarity threshold for store-time deduplication (default: 0.95).
    #[serde(default = "default_store_dedup_threshold")]
    pub dedup_threshold: f32,
}

impl Default for MemoryPolicy {
    fn default() -> Self {
        Self {
            working_ttl_seconds: default_working_ttl(),
            episodic_ttl_seconds: default_episodic_ttl(),
            semantic_ttl_seconds: default_semantic_ttl(),
            procedural_ttl_seconds: default_procedural_ttl(),
            working_decay: default_working_decay(),
            episodic_decay: default_episodic_decay(),
            semantic_decay: default_semantic_decay(),
            procedural_decay: default_procedural_decay(),
            spaced_repetition_factor: default_sr_factor(),
            spaced_repetition_base_interval_seconds: default_sr_base_interval(),
            consolidation_enabled: default_consolidation_enabled(),
            consolidation_threshold: default_policy_consolidation_threshold(),
            consolidation_interval_hours: default_consolidation_interval_hours(),
            consolidated_count: 0,
            rate_limit_enabled: false,
            rate_limit_stores_per_minute: None,
            rate_limit_recalls_per_minute: None,
            dedup_on_store: false,
            dedup_threshold: default_store_dedup_threshold(),
        }
    }
}

impl MemoryPolicy {
    /// Return the configured TTL for the given memory type, in seconds.
    pub fn ttl_for_type(&self, memory_type: &MemoryType) -> Option<u64> {
        match memory_type {
            MemoryType::Working => self.working_ttl_seconds,
            MemoryType::Episodic => self.episodic_ttl_seconds,
            MemoryType::Semantic => self.semantic_ttl_seconds,
            MemoryType::Procedural => self.procedural_ttl_seconds,
        }
    }

    /// Return the configured decay strategy for the given memory type.
    pub fn decay_for_type(&self, memory_type: &MemoryType) -> DecayStrategy {
        match memory_type {
            MemoryType::Working => self.working_decay,
            MemoryType::Episodic => self.episodic_decay,
            MemoryType::Semantic => self.semantic_decay,
            MemoryType::Procedural => self.procedural_decay,
        }
    }

    /// Compute the spaced repetition TTL extension in seconds.
    ///
    /// `extension = access_count × sr_factor × sr_base_interval_seconds`
    pub fn spaced_repetition_extension(&self, access_count: u32) -> u64 {
        if self.spaced_repetition_factor <= 0.0 {
            return 0;
        }
        let ext = access_count as f64
            * self.spaced_repetition_factor
            * self.spaced_repetition_base_interval_seconds as f64;
        ext.round() as u64
    }
}

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

    // ── Memory round-trip ────────────────────────────────────────────────────

    #[test]
    fn test_memory_to_vector_from_vector_roundtrip() {
        let memory = Memory {
            id: "mem_abc123".to_string(),
            memory_type: MemoryType::Episodic,
            content: "Test content".to_string(),
            agent_id: "test-agent".to_string(),
            session_id: Some("sess_001".to_string()),
            importance: 0.8,
            tags: vec!["tag1".to_string(), "tag2".to_string()],
            metadata: Some(serde_json::json!({"key": "value"})),
            created_at: 1_700_000_000,
            last_accessed_at: 1_700_001_000,
            access_count: 5,
            ttl_seconds: None,
            expires_at: None,
        };

        let embedding = vec![0.1f32, 0.2, 0.3];
        let vector = memory.to_vector(embedding.clone());
        assert_eq!(vector.id, memory.id);
        assert_eq!(vector.values, embedding);

        let recovered =
            Memory::from_vector(&vector).expect("should reconstruct memory from vector");
        assert_eq!(recovered.id, memory.id);
        assert_eq!(recovered.content, memory.content);
        assert_eq!(recovered.agent_id, memory.agent_id);
        assert_eq!(recovered.session_id, memory.session_id);
        assert_eq!(recovered.tags, memory.tags);
        assert_eq!(recovered.access_count, memory.access_count);
        assert_eq!(recovered.created_at, memory.created_at);
        assert_eq!(recovered.last_accessed_at, memory.last_accessed_at);
        // importance round-trips through JSON f64 → f32; allow tiny epsilon
        assert!(
            (recovered.importance - memory.importance).abs() < 1e-5,
            "importance mismatch: {} vs {}",
            recovered.importance,
            memory.importance
        );
    }

    #[test]
    fn test_memory_from_vector_rejects_session_type() {
        let mut meta = serde_json::Map::new();
        meta.insert("_dakera_type".to_string(), serde_json::json!("session"));
        let vector = Vector {
            id: "v1".to_string(),
            values: vec![],
            metadata: Some(serde_json::Value::Object(meta)),
            ttl_seconds: None,
            expires_at: None,
        };
        assert!(
            Memory::from_vector(&vector).is_none(),
            "from_vector should return None for wrong _dakera_type"
        );
    }

    #[test]
    fn test_memory_from_vector_rejects_missing_metadata() {
        let vector = Vector {
            id: "v1".to_string(),
            values: vec![],
            metadata: None,
            ttl_seconds: None,
            expires_at: None,
        };
        assert!(Memory::from_vector(&vector).is_none());
    }

    // ── Session round-trip ───────────────────────────────────────────────────

    #[test]
    fn test_session_to_vector_from_vector_roundtrip() {
        let mut session = Session::new("sess_xyz".to_string(), "agent-007".to_string());
        session.ended_at = Some(1_700_005_000);
        session.summary = Some("Session ended cleanly".to_string());
        session.memory_count = 42;

        let embedding = vec![0.5f32, 0.5, 0.5];
        let vector = session.to_vector(embedding.clone());
        assert_eq!(vector.id, session.id);
        assert_eq!(vector.values, embedding);

        let recovered = Session::from_vector(&vector).expect("should reconstruct session");
        assert_eq!(recovered.id, session.id);
        assert_eq!(recovered.agent_id, session.agent_id);
        assert_eq!(recovered.ended_at, session.ended_at);
        assert_eq!(recovered.summary, session.summary);
        assert_eq!(recovered.memory_count, session.memory_count);
    }

    #[test]
    fn test_session_from_vector_rejects_memory_type() {
        let mut meta = serde_json::Map::new();
        meta.insert("_dakera_type".to_string(), serde_json::json!("memory"));
        let vector = Vector {
            id: "sess_1".to_string(),
            values: vec![],
            metadata: Some(serde_json::Value::Object(meta)),
            ttl_seconds: None,
            expires_at: None,
        };
        assert!(
            Session::from_vector(&vector).is_none(),
            "from_vector should return None for wrong _dakera_type"
        );
    }

    #[test]
    fn test_session_active_has_no_ended_at() {
        let session = Session::new("sess_active".to_string(), "agent-1".to_string());
        let vector = session.to_vector(vec![0.1]);
        let recovered = Session::from_vector(&vector).unwrap();
        assert!(recovered.ended_at.is_none());
        assert_eq!(recovered.memory_count, 0);
    }

    // ── PaginationCursor ─────────────────────────────────────────────────────

    #[test]
    fn test_pagination_cursor_encode_decode_roundtrip() {
        let cursor = PaginationCursor::new(0.75, "mem_abc".to_string());
        let encoded = cursor.encode();
        let decoded = PaginationCursor::decode(&encoded).expect("should decode valid cursor");
        assert!(
            (decoded.last_score - cursor.last_score).abs() < 1e-6,
            "score mismatch after decode"
        );
        assert_eq!(decoded.last_id, cursor.last_id);
    }

    #[test]
    fn test_pagination_cursor_decode_invalid_returns_none() {
        assert!(PaginationCursor::decode("not-valid-base64!!!").is_none());
        assert!(PaginationCursor::decode("").is_none());
        // Valid base64 but not valid JSON cursor
        assert!(PaginationCursor::decode("aGVsbG8=").is_none()); // "hello"
    }

    // ── DistanceMetric serde ─────────────────────────────────────────────────

    #[test]
    fn test_distance_metric_serde_round_trip() {
        let cases = [
            (DistanceMetric::Cosine, "\"cosine\""),
            (DistanceMetric::Euclidean, "\"euclidean\""),
            (DistanceMetric::DotProduct, "\"dot_product\""),
        ];
        for (metric, expected_json) in &cases {
            let serialized = serde_json::to_string(metric).unwrap();
            assert_eq!(
                &serialized, expected_json,
                "serialized form mismatch for {:?}",
                metric
            );
            let deserialized: DistanceMetric = serde_json::from_str(&serialized).unwrap();
            assert_eq!(*metric, deserialized);
        }
    }

    // ── Vector TTL helpers ───────────────────────────────────────────────────

    #[test]
    fn test_vector_is_expired_at_not_yet_expired() {
        let vector = Vector {
            id: "v1".to_string(),
            values: vec![1.0],
            metadata: None,
            ttl_seconds: None,
            expires_at: Some(u64::MAX),
        };
        assert!(!vector.is_expired_at(0));
        assert!(!vector.is_expired_at(1_000_000));
    }

    #[test]
    fn test_vector_is_expired_at_past_expiry() {
        let vector = Vector {
            id: "v1".to_string(),
            values: vec![1.0],
            metadata: None,
            ttl_seconds: None,
            expires_at: Some(100),
        };
        assert!(vector.is_expired_at(100), "at boundary should be expired");
        assert!(vector.is_expired_at(200));
        assert!(!vector.is_expired_at(99));
    }

    #[test]
    fn test_vector_no_expiry_never_expires() {
        let vector = Vector {
            id: "v1".to_string(),
            values: vec![1.0],
            metadata: None,
            ttl_seconds: None,
            expires_at: None,
        };
        assert!(!vector.is_expired_at(u64::MAX));
    }

    // ── ColumnUpsertRequest::to_vectors ──────────────────────────────────────

    #[test]
    fn test_column_upsert_mismatched_vectors_length_errors() {
        let req = ColumnUpsertRequest {
            ids: vec!["a".to_string(), "b".to_string()],
            vectors: vec![vec![1.0, 2.0]], // only 1 vector for 2 ids
            attributes: Default::default(),
            ttl_seconds: None,
            dimension: None,
        };
        assert!(
            req.to_vectors().is_err(),
            "should error when vectors.len() != ids.len()"
        );
    }

    #[test]
    fn test_column_upsert_mismatched_attribute_length_errors() {
        let mut attrs = std::collections::HashMap::new();
        attrs.insert(
            "score".to_string(),
            vec![serde_json::json!(1.0)], // only 1 value for 2 ids
        );
        let req = ColumnUpsertRequest {
            ids: vec!["a".to_string(), "b".to_string()],
            vectors: vec![vec![1.0], vec![2.0]],
            attributes: attrs,
            ttl_seconds: None,
            dimension: None,
        };
        assert!(
            req.to_vectors().is_err(),
            "should error when attribute array length != ids.len()"
        );
    }

    #[test]
    fn test_column_upsert_mismatched_dimension_errors() {
        let req = ColumnUpsertRequest {
            ids: vec!["a".to_string(), "b".to_string()],
            vectors: vec![vec![1.0, 2.0], vec![1.0]], // second vector has dim 1, first has dim 2
            attributes: Default::default(),
            ttl_seconds: None,
            dimension: None,
        };
        assert!(
            req.to_vectors().is_err(),
            "should error on dimension mismatch between vectors"
        );
    }

    #[test]
    fn test_column_upsert_success() {
        let req = ColumnUpsertRequest {
            ids: vec!["a".to_string(), "b".to_string()],
            vectors: vec![vec![1.0, 0.0], vec![0.0, 1.0]],
            attributes: Default::default(),
            ttl_seconds: None,
            dimension: Some(2),
        };
        let result = req.to_vectors().expect("valid request should succeed");
        assert_eq!(result.len(), 2);
        assert_eq!(result[0].id, "a");
        assert_eq!(result[1].id, "b");
        assert_eq!(result[0].values, vec![1.0, 0.0]);
        assert_eq!(result[1].values, vec![0.0, 1.0]);
    }
}
