//! Item metadata serialization and signal classification helpers.

use std::collections::HashMap;

/// Serialize `HashMap<String, String>` as length-prefixed binary pairs.
///
/// This is the **items-only** metadata format (no embedding header).
/// For user/creator entities (which include an optional embedding prefix),
/// see [`entities::serialize_entity`](crate::entities::serialize_entity).
///
/// The two formats are intentionally different: items store only metadata
/// (embeddings go to the HNSW index), while users/creators store an optional
/// embedding inline with their metadata for cold-start preference vectors.
///
/// Format (all lengths little-endian u32):
/// ```text
/// [num_entries: 4 bytes]
/// for each entry:
///   [key_len: 4 bytes][key bytes]
///   [val_len: 4 bytes][value bytes]
/// ```
pub(super) fn serialize_metadata(map: &HashMap<String, String>) -> Vec<u8> {
    #[allow(clippy::cast_possible_truncation)]
    let mut buf = Vec::new();
    buf.extend_from_slice(&(map.len() as u32).to_le_bytes());
    for (k, v) in map {
        buf.extend_from_slice(&(k.len() as u32).to_le_bytes());
        buf.extend_from_slice(k.as_bytes());
        buf.extend_from_slice(&(v.len() as u32).to_le_bytes());
        buf.extend_from_slice(v.as_bytes());
    }
    buf
}

/// Deserialize `HashMap<String, String>` from the binary format produced by
/// [`serialize_metadata`].
///
/// This is the **items-only** metadata format (no embedding header).
/// For user/creator entities, see [`entities::deserialize_entity`](crate::entities::deserialize_entity).
///
/// Returns an empty map if the bytes are empty or malformed -- metadata reads
/// must never panic or fail the query.
pub fn deserialize_metadata(bytes: &[u8]) -> HashMap<String, String> {
    let mut map = HashMap::new();
    if bytes.len() < 4 {
        return map;
    }
    let count = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
    let mut pos = 4;
    for _ in 0..count {
        if pos + 4 > bytes.len() {
            break;
        }
        let key_len =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        pos += 4;
        if pos + key_len > bytes.len() {
            break;
        }
        let key = String::from_utf8_lossy(&bytes[pos..pos + key_len]).to_string();
        pos += key_len;
        if pos + 4 > bytes.len() {
            break;
        }
        let val_len =
            u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
                as usize;
        pos += 4;
        if pos + val_len > bytes.len() {
            break;
        }
        let val = String::from_utf8_lossy(&bytes[pos..pos + val_len]).to_string();
        pos += val_len;
        map.insert(key, val);
    }
    map
}

/// Returns `true` for positive engagement signal types that should update
/// the user's preference vector.
///
/// These are high-intent signals: the user explicitly chose to engage with
/// the content. Views are excluded because they are low-signal (the user
/// may have scrolled past without interest).
pub(super) fn is_positive_engagement_signal(signal_type: &str) -> bool {
    matches!(
        signal_type,
        "like" | "share" | "completion" | "search_click"
    )
}
