//! Embedding lifecycle operations: insert, update, delete.
//!
//! These sit between the entity write API (`write_item()`) and the raw
//! [`VectorIndex`] trait. Each operation enforces L2 normalization, persists
//! the source-of-truth vector in the entity store, and updates the HNSW index.

use super::super::{VectorError, VectorIndex};
use super::normalize::l2_normalize;
use super::serde::{embedding_store_key, serialize_embedding};
use crate::schema::EntityId;
use crate::storage::StorageEngine;

/// Insert an embedding for an entity.
///
/// 1. Validates that `raw_vector.len() == expected_dimensions`.
/// 2. L2-normalizes the vector to unit length.
/// 3. Stores the normalized f32 vector in the entity store (source of truth).
/// 4. Inserts the normalized vector into the HNSW index.
///
/// # Errors
///
/// - [`VectorError::DimensionMismatch`] if dimensions do not match.
/// - [`VectorError::ZeroNormVector`] if the raw vector has zero norm.
/// - [`VectorError::Backend`] if the entity store write fails.
/// - Any error from [`VectorIndex::insert`].
pub fn insert_embedding(
    entity_id: EntityId,
    slot_name: &str,
    raw_vector: &[f32],
    expected_dimensions: usize,
    index: &dyn VectorIndex,
    storage: &dyn StorageEngine,
) -> Result<(), VectorError> {
    // Validate dimensions
    if raw_vector.len() != expected_dimensions {
        return Err(VectorError::DimensionMismatch {
            expected: expected_dimensions,
            got: raw_vector.len(),
        });
    }

    // Normalize
    let normalized = l2_normalize(raw_vector)?;

    // Store in entity store (source of truth)
    let key = embedding_store_key(entity_id, slot_name);
    let value = serialize_embedding(&normalized);
    storage
        .put(&key, &value)
        .map_err(|e| VectorError::Backend(format!("entity store write failed: {e}")))?;

    // Insert into HNSW index
    index.insert(entity_id.as_u64(), &normalized)?;

    Ok(())
}

/// Update an embedding for an entity.
///
/// 1. Validates dimensions.
/// 2. L2-normalizes the new vector.
/// 3. Updates the entity store (overwrite).
/// 4. Tombstones the old vector in HNSW (ignores `NotFound` for first-time writes).
/// 5. Inserts the new vector into HNSW.
///
/// Note: between steps 4 and 5, the entity is absent from ANN results.
/// This window is microseconds and is acceptable per Spec 07, Section 6.
///
/// # Errors
///
/// - [`VectorError::DimensionMismatch`] if dimensions do not match.
/// - [`VectorError::ZeroNormVector`] if the raw vector has zero norm.
/// - [`VectorError::Backend`] if the entity store write fails.
/// - Any error from [`VectorIndex::insert`].
pub fn update_embedding(
    entity_id: EntityId,
    slot_name: &str,
    raw_vector: &[f32],
    expected_dimensions: usize,
    index: &dyn VectorIndex,
    storage: &dyn StorageEngine,
) -> Result<(), VectorError> {
    if raw_vector.len() != expected_dimensions {
        return Err(VectorError::DimensionMismatch {
            expected: expected_dimensions,
            got: raw_vector.len(),
        });
    }

    let normalized = l2_normalize(raw_vector)?;

    // Update entity store
    let key = embedding_store_key(entity_id, slot_name);
    let value = serialize_embedding(&normalized);
    storage
        .put(&key, &value)
        .map_err(|e| VectorError::Backend(format!("entity store write failed: {e}")))?;

    // Tombstone old in HNSW, insert new.
    // delete() may return NotFound if the entity was never indexed (first embedding
    // via update path). That is fine -- ignore NotFound on the delete step.
    let delete_result = index.delete(entity_id.as_u64());
    match delete_result {
        Ok(()) | Err(VectorError::NotFound { .. }) => {}
        Err(e) => return Err(e),
    }
    index.insert(entity_id.as_u64(), &normalized)?;

    Ok(())
}

/// Delete an embedding for an entity.
///
/// 1. Tombstones the vector in HNSW.
/// 2. If `hard_delete` is true, also removes the embedding from the entity store.
///
/// For archive (soft delete): tombstone HNSW only, keep entity store data.
/// For hard delete: tombstone HNSW and remove entity store key.
///
/// # Errors
///
/// - [`VectorError::NotFound`] if the vector is not in the HNSW index.
/// - [`VectorError::Backend`] if the entity store delete fails.
pub fn delete_embedding(
    entity_id: EntityId,
    slot_name: &str,
    index: &dyn VectorIndex,
    storage: &dyn StorageEngine,
    hard_delete: bool,
) -> Result<(), VectorError> {
    // Tombstone in HNSW
    index.delete(entity_id.as_u64())?;

    // Optionally remove from entity store
    if hard_delete {
        let key = embedding_store_key(entity_id, slot_name);
        storage
            .delete(&key)
            .map_err(|e| VectorError::Backend(format!("entity store delete failed: {e}")))?;
    }

    Ok(())
}

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

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::cast_precision_loss)]
mod tests {
    use super::super::serde::{deserialize_embedding, embedding_store_key};
    use super::*;
    use crate::schema::EntityId;
    use crate::storage::memory::InMemoryBackend;
    use crate::storage::vector::{BruteForceIndex, VectorIndexConfig};

    // -------------------------------------------------------------------
    // insert_embedding
    // -------------------------------------------------------------------

    #[test]
    fn insert_embedding_validates_dimensions() {
        let config = VectorIndexConfig {
            dimensions: 3,
            ..VectorIndexConfig::default()
        };
        let index = BruteForceIndex::new(config);
        let storage = InMemoryBackend::new();

        let result = insert_embedding(
            EntityId::new(1),
            "content",
            &[1.0, 2.0],
            3,
            &index,
            &storage,
        );
        assert!(matches!(
            result,
            Err(VectorError::DimensionMismatch {
                expected: 3,
                got: 2
            })
        ));
    }

    #[test]
    fn insert_embedding_stores_normalized_vector() {
        let config = VectorIndexConfig {
            dimensions: 3,
            ..VectorIndexConfig::default()
        };
        let index = BruteForceIndex::new(config);
        let storage = InMemoryBackend::new();

        insert_embedding(
            EntityId::new(1),
            "content",
            &[3.0, 4.0, 0.0],
            3,
            &index,
            &storage,
        )
        .unwrap();

        // Read from entity store
        let key = embedding_store_key(EntityId::new(1), "content");
        let bytes = storage.get(&key).unwrap().unwrap();
        let stored = deserialize_embedding(&bytes).unwrap();

        // Should be normalized (norm = 5, so [0.6, 0.8, 0.0])
        assert!((stored[0] - 0.6).abs() < 1e-5);
        assert!((stored[1] - 0.8).abs() < 1e-5);
        assert!(stored[2].abs() < 1e-5);
    }

    #[test]
    fn insert_embedding_zero_vector_fails() {
        let config = VectorIndexConfig {
            dimensions: 3,
            ..VectorIndexConfig::default()
        };
        let index = BruteForceIndex::new(config);
        let storage = InMemoryBackend::new();

        let result = insert_embedding(
            EntityId::new(1),
            "content",
            &[0.0, 0.0, 0.0],
            3,
            &index,
            &storage,
        );
        assert!(matches!(result, Err(VectorError::ZeroNormVector)));
    }

    // -------------------------------------------------------------------
    // update_embedding
    // -------------------------------------------------------------------

    #[test]
    fn update_embedding_replaces_vector() {
        let config = VectorIndexConfig {
            dimensions: 3,
            ..VectorIndexConfig::default()
        };
        let index = BruteForceIndex::new(config);
        let storage = InMemoryBackend::new();

        // Insert original
        insert_embedding(
            EntityId::new(1),
            "content",
            &[1.0, 0.0, 0.0],
            3,
            &index,
            &storage,
        )
        .unwrap();

        // Update
        update_embedding(
            EntityId::new(1),
            "content",
            &[0.0, 1.0, 0.0],
            3,
            &index,
            &storage,
        )
        .unwrap();

        // Search should find the updated vector
        let results = index.search(&[0.0, 1.0, 0.0], 1, 0).unwrap();
        assert_eq!(results[0].id, 1);
        assert!(results[0].distance < 1e-5, "should match updated vector");
    }

    // -------------------------------------------------------------------
    // delete_embedding
    // -------------------------------------------------------------------

    #[test]
    fn delete_embedding_removes_from_index() {
        let config = VectorIndexConfig {
            dimensions: 3,
            ..VectorIndexConfig::default()
        };
        let index = BruteForceIndex::new(config);
        let storage = InMemoryBackend::new();

        insert_embedding(
            EntityId::new(1),
            "content",
            &[1.0, 0.0, 0.0],
            3,
            &index,
            &storage,
        )
        .unwrap();

        delete_embedding(EntityId::new(1), "content", &index, &storage, false).unwrap();

        // Should not appear in search results
        let results = index.search(&[1.0, 0.0, 0.0], 10, 0).unwrap();
        assert!(results.is_empty());

        // Soft delete: entity store still has the embedding
        let key = embedding_store_key(EntityId::new(1), "content");
        assert!(storage.get(&key).unwrap().is_some());
    }

    #[test]
    fn delete_embedding_hard_removes_from_store() {
        let config = VectorIndexConfig {
            dimensions: 3,
            ..VectorIndexConfig::default()
        };
        let index = BruteForceIndex::new(config);
        let storage = InMemoryBackend::new();

        insert_embedding(
            EntityId::new(1),
            "content",
            &[1.0, 0.0, 0.0],
            3,
            &index,
            &storage,
        )
        .unwrap();

        delete_embedding(EntityId::new(1), "content", &index, &storage, true).unwrap();

        // Entity store should not have the embedding
        let key = embedding_store_key(EntityId::new(1), "content");
        assert!(storage.get(&key).unwrap().is_none());
    }

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

    mod proptests {
        use super::super::super::serde::{deserialize_embedding, embedding_store_key};
        use super::*;
        use proptest::prelude::*;

        proptest! {
            #[test]
            fn insert_embedding_searchable(
                dim in 2usize..64,
                n in 1usize..50,
            ) {
                let config = VectorIndexConfig {
                    dimensions: dim,
                    ..VectorIndexConfig::default()
                };
                let index = BruteForceIndex::new(config);
                let storage = InMemoryBackend::new();

                for id in 0..n as u64 {
                    let raw: Vec<f32> = (0..dim)
                        .map(|i| ((id as usize + i) % 100) as f32 / 100.0 + 0.01)
                        .collect();
                    insert_embedding(
                        EntityId::new(id + 1),
                        "content",
                        &raw,
                        dim,
                        &index,
                        &storage,
                    )
                    .unwrap();
                }

                // Verify all are searchable
                prop_assert_eq!(index.len(), n);

                // Verify entity store has the normalized vectors
                for id in 0..n as u64 {
                    let key = embedding_store_key(EntityId::new(id + 1), "content");
                    let bytes = storage.get(&key).unwrap();
                    prop_assert!(
                        bytes.is_some(),
                        "entity store should have embedding for id {id}"
                    );
                    let stored = deserialize_embedding(&bytes.unwrap()).unwrap();
                    let norm: f32 = stored.iter().map(|x| x * x).sum::<f32>().sqrt();
                    prop_assert!(
                        (1.0 - norm).abs() < 1e-5,
                        "stored embedding should be normalized"
                    );
                }
            }
        }
    }
}
