//! User entity write/read operations on `TidalDb`.
//!
//! This file also hosts the shared persistence helpers reused across the
//! user, creator, collection, and cohort entity write paths (see
//! [`write_entity_meta`], [`read_entity_meta`], and
//! [`TidalDb::persist_to_items`]). They live here rather than in a new module
//! so the `db` namespace stays flat.

use std::collections::HashMap;

use super::TidalDb;
use crate::{
    schema::{EntityId, EntityKind, TidalError},
    storage::{
        StorageEngine, Tag, encode_key,
        vector::{
            QuantizationLevel, insert_embedding,
            registry::{EmbeddingSlotState, EmbeddingSource, HnswParams, build_slot_index},
        },
    },
};

// ── Shared entity-metadata persistence helpers ───────────────────────────────

/// Write an entity's metadata (with no embedding) under its `Tag::Meta` key.
///
/// This is the common body of [`TidalDb::write_user`] and
/// [`TidalDb::write_creator`]: both serialize `(None, metadata)` and `put` it
/// at `encode_key(id, Tag::Meta, b"")` against their own engine. Centralizing it
/// keeps the on-disk entity format defined in exactly one place, so users and
/// creators can never drift apart. Storage errors map to `TidalError::Storage`
/// via the `From<StorageError>` impl, matching the prior per-call behavior.
///
/// # Errors
///
/// - `TidalError::Storage` on storage engine failure.
pub(super) fn write_entity_meta(
    engine: &dyn StorageEngine,
    id: EntityId,
    metadata: &HashMap<String, String>,
) -> crate::Result<()> {
    let key = encode_key(id, Tag::Meta, b"");
    let value = crate::entities::serialize_entity(None, metadata);
    engine.put(&key, &value).map_err(TidalError::from)
}

/// Read an entity's metadata from its `Tag::Meta` key, returning `None` when
/// the entity does not exist.
///
/// The common body of [`TidalDb::get_user_metadata`] and
/// [`TidalDb::get_creator_metadata`]. Discards the optional embedding prefix and
/// returns only the metadata map.
///
/// # Errors
///
/// - `TidalError::Storage` on storage engine failure.
pub(super) fn read_entity_meta(
    engine: &dyn StorageEngine,
    id: EntityId,
) -> crate::Result<Option<HashMap<String, String>>> {
    let key = encode_key(id, Tag::Meta, b"");
    match engine.get(&key) {
        Ok(Some(bytes)) => {
            let (_emb, meta) = crate::entities::deserialize_entity(&bytes);
            Ok(Some(meta))
        }
        Ok(None) => Ok(None),
        Err(e) => Err(TidalError::from(e)),
    }
}

impl TidalDb {
    /// Persist a key/value into the items engine when durable storage is wired,
    /// or no-op in ephemeral mode.
    ///
    /// This is the shared body of the best-effort `if let Some(storage) {
    /// items_engine().put(...).map_err(internal) }` shape repeated by the
    /// collection, saved-search, and cohort persist paths. Errors map to
    /// `TidalError::Internal { op, .. }` so each caller keeps its own operation
    /// label for diagnostics. In ephemeral mode (no storage) the write is a
    /// no-op, preserving the prior "in-memory only" contract.
    ///
    /// # Errors
    ///
    /// - `TidalError::Internal` on storage engine failure (labeled with `op`).
    pub(super) fn persist_to_items(
        &self,
        key: &[u8],
        value: &[u8],
        op: &'static str,
    ) -> crate::Result<()> {
        if let Some(storage) = self.storage.as_ref() {
            storage
                .items_engine()
                .put(key, value)
                .map_err(|e| TidalError::internal(op, format!("{e}")))?;
        }
        Ok(())
    }

    /// Auto-register an embedding slot (if absent), then L2-normalize the
    /// embedding, store it in `engine` (the entity-kind's source of truth), and
    /// insert it into that slot's vector index.
    ///
    /// This is the shared body of [`TidalDb::write_item_embedding`] and
    /// [`TidalDb::write_creator_embedding`], which differ only in their target
    /// `kind`, `slot_name`, storage `engine`, and the `op` label used for error
    /// diagnostics. Centralizing it keeps the registry lock discipline defined
    /// in exactly one place: the write lock is taken only for the one-shot slot
    /// registration and dropped before the read phase; a read lock is then taken
    /// to resolve the slot dimensions, dropped, and re-taken across the
    /// `insert_embedding` call (which must hold it for the duration of the index
    /// mutation). Behavior is identical to the prior per-method bodies.
    ///
    /// # Errors
    ///
    /// - `TidalError::Internal` if the embedding registry lock is poisoned, the
    ///   slot cannot be registered, the slot is missing after registration, or
    ///   the embedding has zero norm.
    /// - `TidalError::Storage` on storage engine failure.
    #[allow(clippy::significant_drop_tightening)] // lock must be held across insert_embedding call
    pub(super) fn write_entity_embedding(
        &self,
        id: EntityId,
        kind: EntityKind,
        slot_name: &str,
        embedding: &[f32],
        engine: &dyn StorageEngine,
        op: &'static str,
    ) -> crate::Result<()> {
        // Auto-register the slot if absent. The write lock is scoped to this
        // block and dropped before the read phase below.
        {
            let mut registry = self
                .embedding_registry
                .write()
                .map_err(|_| TidalError::internal(op, "embedding_registry write lock poisoned"))?;
            if registry.get(kind, slot_name).is_none() {
                // Route backend selection through the size-gated factory rather
                // than hardcoding brute force. A freshly auto-registered slot has
                // no vectors yet, so the factory builds the exact `BruteForceIndex`
                // (correct for a tiny / cold slot — no graph-build cost, exact
                // results). When the slot's durable vector count crosses
                // `USEARCH_MIN_VECTORS`, the next process restart's
                // `rebuild_from_store` promotes it to the production HNSW
                // (`UsearchIndex`) automatically — no config plumbing required.
                let state = EmbeddingSlotState {
                    index: build_slot_index(embedding.len(), 0),
                    dimensions: embedding.len(),
                    quantization: QuantizationLevel::F32,
                    source: EmbeddingSource::External,
                    params: HnswParams::default(),
                };
                registry
                    .register(kind, slot_name.to_string(), state)
                    .map_err(|e| {
                        TidalError::internal(op, format!("slot registration failed: {e}"))
                    })?;
            }
        }

        // Read the slot dimensions, dropping the lock before the storage write.
        let dimensions = {
            let registry = self
                .embedding_registry
                .read()
                .map_err(|_| TidalError::internal(op, "embedding_registry read lock poisoned"))?;
            registry
                .get(kind, slot_name)
                .ok_or_else(|| TidalError::internal(op, "embedding slot missing"))?
                .dimensions
        };

        // Re-acquire the read lock to get the index reference, perform the
        // insert, then drop the lock.
        {
            let registry = self
                .embedding_registry
                .read()
                .map_err(|_| TidalError::internal(op, "embedding_registry read lock poisoned"))?;
            let slot = registry
                .get(kind, slot_name)
                .ok_or_else(|| TidalError::internal(op, "embedding slot missing"))?;
            insert_embedding(
                id,
                slot_name,
                embedding,
                dimensions,
                slot.index.as_ref(),
                engine,
            )
            .map_err(|e| TidalError::internal(op, format!("{e}")))?;
        }

        Ok(())
    }
}

impl TidalDb {
    /// Write (or overwrite) a user entity.
    ///
    /// Stores metadata and optional embedding under the user's `Tag::Meta` key
    /// in the users storage backend.
    ///
    /// # Errors
    ///
    /// - `TidalError::Internal` if no storage backend is wired.
    /// - `TidalError::Storage` on storage engine failure.
    pub fn write_user(
        &self,
        id: EntityId,
        metadata: &HashMap<String, String>,
    ) -> crate::Result<()> {
        self.require_writeable("write_user")?;
        let storage = self.storage()?;
        write_entity_meta(storage.users_engine(), id, metadata)?;
        self.cohort_resolver.invalidate(id.as_u64());
        Ok(())
    }

    /// Read user metadata for a given entity ID.
    ///
    /// Returns `None` if the user does not exist in storage.
    ///
    /// # Errors
    ///
    /// - `TidalError::Internal` if no storage backend is wired.
    /// - `TidalError::Storage` on storage engine failure.
    pub fn get_user_metadata(
        &self,
        id: EntityId,
    ) -> crate::Result<Option<HashMap<String, String>>> {
        let storage = self.storage()?;
        read_entity_meta(storage.users_engine(), id)
    }

    /// Enumerate every persisted user's `(EntityId, metadata)`.
    ///
    /// User entities are stored as `serialize_entity(embedding, metadata)`, so
    /// this deserializes each and returns only the metadata map. Intended for
    /// boot-time rebuild of an embedder's external `EntityId`-to-domain-key
    /// mapping after a persistent restart (mirrors [`Self::all_item_metadata`]
    /// for the user namespace).
    ///
    /// # Errors
    ///
    /// - `TidalError::Internal` if no storage backend is wired.
    /// - `TidalError::Storage` on storage engine failure.
    pub fn all_user_metadata(&self) -> crate::Result<Vec<(EntityId, HashMap<String, String>)>> {
        use crate::storage::keys::parse_key;
        let storage = self.storage()?;
        let mut out = Vec::new();
        for entry in storage.users_engine().scan_prefix(&[]) {
            let (key, value) = entry.map_err(TidalError::from)?;
            if let Some((entity_id, Tag::Meta, suffix)) = parse_key(&key) {
                // Skip embedding rows: they share Tag::Meta but their value is a
                // serialized vector (persisted under the `EMB:{slot}` suffix by
                // `embedding_store_key`), not a serialized entity. Deserializing
                // those as an entity yields garbage that must never reach callers
                // (embedder key-mapping reconstruction). Mirrors the same skip in
                // `all_item_metadata`.
                if suffix.starts_with(b"EMB:") {
                    continue;
                }
                let (_emb, meta) = crate::entities::deserialize_entity(&value);
                out.push((entity_id, meta));
            }
        }
        Ok(out)
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use std::collections::HashMap;

    use crate::{
        TidalDb,
        schema::EntityId,
        storage::vector::{embedding_store_key, serialize_embedding},
    };

    /// Minimal valid schema so ephemeral mode wires in-memory storage.
    fn minimal_schema() -> crate::schema::Schema {
        use crate::schema::{DecaySpec, EntityKind, SchemaBuilder, Window};
        let mut b = SchemaBuilder::new();
        let _ = b
            .signal(
                "view",
                EntityKind::Item,
                DecaySpec::Exponential {
                    half_life: std::time::Duration::from_secs(3600),
                },
            )
            .windows(&[Window::AllTime])
            .velocity(false)
            .add();
        b.build().expect("schema must be valid")
    }

    #[test]
    fn all_user_metadata_skips_embedding_rows() {
        let db = TidalDb::builder()
            .ephemeral()
            .with_schema(minimal_schema())
            .open()
            .unwrap();

        // A real user with plain metadata.
        let mut meta = HashMap::new();
        meta.insert("name".to_string(), "alice".to_string());
        db.write_user(EntityId::new(1), &meta).unwrap();

        // An embedding row written under the SAME Tag::Meta key space the user
        // embedding durability path uses (`EMB:{slot}` suffix). Its value is a
        // serialized vector, not a serialized entity — `all_user_metadata` must
        // not surface it as user metadata.
        let emb_key = embedding_store_key(EntityId::new(1), "content");
        let emb_value = serialize_embedding(&[0.1, 0.2, 0.3]);
        db.storage()
            .unwrap()
            .users_engine()
            .put(&emb_key, &emb_value)
            .unwrap();

        let all = db.all_user_metadata().unwrap();
        assert_eq!(
            all.len(),
            1,
            "embedding row must be skipped; expected only the plain user row, got {all:?}"
        );
        assert_eq!(all[0].0, EntityId::new(1));
        assert_eq!(all[0].1.get("name").map(String::as_str), Some("alice"));

        db.close().unwrap();
    }
}
