//! Embedding serialization, deserialization, and key construction.
//!
//! The entity store (via [`StorageEngine`]) is the source of truth for
//! full-precision f32 vectors. This module handles the byte-level encoding
//! and the key scheme that co-locates embedding data with entity metadata.

use super::super::VectorError;
use crate::schema::EntityId;
use crate::storage::{Tag, encode_key};

/// Build the entity store key for an embedding slot.
///
/// Format: `encode_key(entity_id, Tag::Meta, b"EMB:slot_name")`
///
/// This co-locates embedding data with entity metadata under the same entity
/// prefix, enabling efficient prefix scans for entity-level operations.
/// The `EMB:` prefix in the suffix distinguishes embedding keys from other
/// metadata keys under `Tag::Meta`.
#[must_use]
pub fn embedding_store_key(entity_id: EntityId, slot_name: &str) -> Vec<u8> {
    let suffix = format!("EMB:{slot_name}");
    encode_key(entity_id, Tag::Meta, suffix.as_bytes())
}

/// Serialize an embedding vector for entity store storage.
///
/// Format: `[dimensions: 4 bytes LE u32][vector: dimensions * 4 bytes, f32 LE]`
///
/// The dimension header enables validation on deserialization without requiring
/// the caller to know the expected dimensionality.
#[must_use]
pub fn serialize_embedding(v: &[f32]) -> Vec<u8> {
    let mut buf = Vec::with_capacity(4 + v.len() * 4);
    #[allow(clippy::cast_possible_truncation)]
    let dims = v.len() as u32;
    buf.extend_from_slice(&dims.to_le_bytes());
    for &x in v {
        buf.extend_from_slice(&x.to_le_bytes());
    }
    buf
}

/// Deserialize an embedding vector from entity store bytes.
///
/// Validates the dimension header and total byte length.
///
/// # Errors
///
/// Returns [`VectorError::CorruptedIndex`] if the data is too short for
/// the dimension header or the total length does not match the declared
/// dimensionality.
pub fn deserialize_embedding(bytes: &[u8]) -> Result<Vec<f32>, VectorError> {
    if bytes.len() < 4 {
        return Err(VectorError::CorruptedIndex(
            "embedding data too short for dimension header".into(),
        ));
    }
    let dim = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
    let expected_len = 4 + dim * 4;
    if bytes.len() != expected_len {
        return Err(VectorError::CorruptedIndex(format!(
            "embedding data length {} != expected {expected_len}",
            bytes.len()
        )));
    }
    let mut v = Vec::with_capacity(dim);
    for i in 0..dim {
        let offset = 4 + i * 4;
        let x = f32::from_le_bytes([
            bytes[offset],
            bytes[offset + 1],
            bytes[offset + 2],
            bytes[offset + 3],
        ]);
        v.push(x);
    }
    Ok(v)
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::cast_precision_loss)]
mod tests {
    use super::*;

    // -------------------------------------------------------------------
    // Serialization round-trip
    // -------------------------------------------------------------------

    #[test]
    fn serialize_deserialize_embedding() {
        let v = vec![1.0, 2.0, 3.0];
        let bytes = serialize_embedding(&v);
        assert_eq!(bytes.len(), 4 + 3 * 4); // 4 dim header + 12 data
        let restored = deserialize_embedding(&bytes).unwrap();
        assert_eq!(v, restored);
    }

    #[test]
    fn deserialize_embedding_truncated() {
        let result = deserialize_embedding(&[0x03, 0x00, 0x00]); // too short for header
        assert!(matches!(result, Err(VectorError::CorruptedIndex(_))));
    }

    #[test]
    fn deserialize_embedding_wrong_length() {
        let mut bytes = serialize_embedding(&[1.0, 2.0]);
        bytes.pop(); // truncate one byte
        let result = deserialize_embedding(&bytes);
        assert!(matches!(result, Err(VectorError::CorruptedIndex(_))));
    }

    // -------------------------------------------------------------------
    // Key construction
    // -------------------------------------------------------------------

    #[test]
    fn embedding_store_key_format() {
        use crate::storage::keys::parse_key;

        let key = embedding_store_key(EntityId::new(42), "content");
        let (eid, tag, suffix) = parse_key(&key).unwrap();
        assert_eq!(eid, EntityId::new(42));
        assert_eq!(tag, Tag::Meta);
        assert_eq!(suffix, b"EMB:content");
    }

    #[test]
    fn embedding_store_key_different_slots() {
        let key_content = embedding_store_key(EntityId::new(1), "content");
        let key_visual = embedding_store_key(EntityId::new(1), "visual");
        assert_ne!(key_content, key_visual);
    }

    // -------------------------------------------------------------------
    // Property tests
    // -------------------------------------------------------------------

    mod proptests {
        use super::*;
        use proptest::prelude::*;

        proptest! {
            #[test]
            fn embedding_serde_roundtrip(
                v in prop::collection::vec(-1.0f32..1.0, 1..512),
            ) {
                let bytes = serialize_embedding(&v);
                let restored = deserialize_embedding(&bytes).unwrap();
                prop_assert_eq!(v.len(), restored.len());
                for (a, b) in v.iter().zip(restored.iter()) {
                    prop_assert!((a - b).abs() < 1e-7,
                        "serde mismatch: {a} vs {b}");
                }
            }
        }
    }
}
