//! Personalization context building for the RETRIEVE executor.
//!
//! Constructs a `UserContext` from the interaction ledger and creator-items
//! bitmap. The resulting per-item boost map is normalized to `[0.0, 1.0]`.

use std::collections::HashMap;

use crate::{
    entities::{CreatorItemsBitmap, InteractionLedger},
    ranking::executor::UserContext,
    schema::Timestamp,
};

/// Build a `UserContext` for personalized scoring.
///
/// Fetches the user's top interacted creators from the `InteractionLedger`,
/// then expands each creator into their item set via `CreatorItemsBitmap`.
/// The resulting per-item boost map is normalized to `[0.0, 1.0]`.
pub(crate) fn build_user_context(
    user_id: u64,
    now: Timestamp,
    interaction_ledger: Option<&InteractionLedger>,
    creator_items: Option<&CreatorItemsBitmap>,
) -> UserContext {
    let now_ns = now.as_nanos();

    // Get top creators from the interaction ledger.
    let top_creators = interaction_ledger
        .map(|il| il.top_creators(user_id, 50, now_ns))
        .unwrap_or_default();

    // Build per-item boost map from creator items.
    let mut creator_interaction_boosts: HashMap<u32, f64> = HashMap::new();
    if let Some(creator_items) = creator_items {
        for (creator_id, weight) in &top_creators {
            if let Some(bitmap) = creator_items.get(*creator_id) {
                for item_id in &bitmap {
                    creator_interaction_boosts.insert(item_id, *weight);
                }
            }
        }
    }

    // Normalize boost values to [0.0, 1.0] so the highest-interacted
    // creator gets boost=1.0 and the interaction boost weight constant
    // controls the absolute magnitude.
    if !creator_interaction_boosts.is_empty() {
        let max_weight = creator_interaction_boosts
            .values()
            .copied()
            .fold(0.0_f64, f64::max);
        if max_weight > f64::EPSILON {
            for val in creator_interaction_boosts.values_mut() {
                *val /= max_weight;
            }
        }
    }

    UserContext {
        user_id,
        creator_interaction_boosts,
        // Populated separately by the executor when a preference vector and
        // per-item embeddings are both available (see `compute_preference_boosts`).
        preference_boosts: HashMap::new(),
    }
}

/// Compute per-candidate preference-vector cosine boosts.
///
/// For each candidate, reads its stored content embedding from `items_storage`
/// (under `slot_name` — the schema's first Item embedding slot, resolved by the
/// caller via `TidalDb::item_embedding_slot`) and computes the cosine similarity
/// against the user's preference vector. The resulting map
/// (`item_id -> cosine in [-1.0, 1.0]`) is consumed by
/// `ProfileExecutor::score_personalized` as an additive ranking boost.
///
/// `slot_name` MUST match the slot the embeddings were written under
/// (`write_item_embedding`) and that preference vectors were folded from
/// (`try_update_preference_vector`); all three share `item_embedding_slot()` so
/// the chain stays coherent regardless of how an app names its slot.
///
/// Returns an empty map when the user has no preference vector, no preference
/// store is wired, or no items storage is available -- in which case the
/// preference boost is a true no-op rather than a silent constant.
///
/// The map is keyed by the **full `u64` entity id** (not a truncated `u32`): two
/// candidates whose ids differ only above bit 32 would otherwise alias onto one
/// slot and cross-contaminate each other's preference boost (query-executor
/// SUGGESTION, M0-M10 review pass 2). The consumer in
/// `ProfileExecutor::score_personalized` looks the boost up by the same full id.
pub(crate) fn compute_preference_boosts(
    user_id: u64,
    candidates: &[crate::schema::EntityId],
    preference_vectors: Option<&crate::entities::PreferenceVectors>,
    items_storage: Option<&dyn crate::storage::StorageEngine>,
    slot_name: &str,
) -> HashMap<u64, f64> {
    let mut boosts = HashMap::new();
    let (Some(prefs), Some(storage)) = (preference_vectors, items_storage) else {
        return boosts;
    };
    // No preference vector recorded for this user yet -> no boost to apply.
    if prefs.get(user_id).is_none() {
        return boosts;
    }

    for &entity_id in candidates {
        let key = crate::storage::vector::embedding_store_key(entity_id, slot_name);
        let Ok(Some(bytes)) = storage.get(&key) else {
            continue;
        };
        let Ok(embedding) = crate::storage::vector::deserialize_embedding(&bytes) else {
            continue;
        };
        // Dimension mismatch (e.g. preference vector sized for a different slot)
        // yields `None`; skip rather than contributing a bogus boost.
        if let Some(cosine) = prefs.cosine_similarity(user_id, &embedding) {
            // Full u64 key: never truncate, so high-bit-distinct ids cannot alias.
            boosts.insert(entity_id.as_u64(), f64::from(cosine));
        }
    }
    boosts
}
