//! Cohort resolver: evaluates user metadata against all registered cohort predicates.
//!
//! Results are cached per user ID in a `DashMap` for fast repeated lookups.
//! The cache is invalidated per-user when their metadata changes.

use std::collections::HashMap;
use std::sync::Arc;

use dashmap::DashMap;

use super::types::{CohortName, CohortRegistry, Predicate};

/// Evaluates user metadata against all registered `CohortDef`s and caches
/// the resulting cohort memberships per user.
pub struct CohortResolver {
    registry: Arc<CohortRegistry>,
    cache: DashMap<u64, Vec<CohortName>>,
}

impl CohortResolver {
    /// Create a resolver backed by the given registry.
    #[must_use]
    pub fn new(registry: Arc<CohortRegistry>) -> Self {
        Self {
            registry,
            cache: DashMap::new(),
        }
    }

    /// Resolve which cohorts a user belongs to.
    ///
    /// Returns cached results if available. Otherwise evaluates all predicates
    /// against the user's metadata, caches the result, and returns it.
    #[must_use]
    pub fn resolve(&self, user_id: u64, metadata: &HashMap<String, String>) -> Vec<CohortName> {
        // Fast path: return cached result.
        if let Some(cached) = self.cache.get(&user_id) {
            return cached.clone();
        }

        // Slow path: evaluate all predicates.
        let mut memberships = Vec::new();
        for entry in self.registry.names() {
            if let Some(def_ref) = self.registry.get(&entry)
                && eval_predicate(&def_ref.predicate, metadata)
            {
                memberships.push(entry.clone());
            }
        }

        self.cache.insert(user_id, memberships.clone());
        memberships
    }

    /// Invalidate cached cohort memberships for a user.
    ///
    /// Call this when user metadata is updated so the next `resolve()` call
    /// re-evaluates all predicates.
    pub fn invalidate(&self, user_id: u64) {
        self.cache.remove(&user_id);
    }

    /// Number of cached user resolutions (for diagnostics).
    #[must_use]
    pub fn cache_len(&self) -> usize {
        self.cache.len()
    }
}

impl std::fmt::Debug for CohortResolver {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CohortResolver")
            .field("cache_len", &self.cache.len())
            .finish_non_exhaustive()
    }
}

/// Evaluate a predicate against user metadata.
///
/// Returns `true` if the user's metadata satisfies the predicate.
fn eval_predicate(pred: &Predicate, metadata: &HashMap<String, String>) -> bool {
    match pred {
        Predicate::Eq { field, value } => metadata.get(field) == Some(value),
        Predicate::Any { field, values } => metadata.get(field).is_some_and(|v| values.contains(v)),
        Predicate::Range { field, lo, hi } => metadata
            .get(field)
            .and_then(|v| v.parse::<f64>().ok())
            .is_some_and(|n| n >= *lo && n <= *hi),
        Predicate::And(preds) => preds.iter().all(|p| eval_predicate(p, metadata)),
        Predicate::Or(preds) => preds.iter().any(|p| eval_predicate(p, metadata)),
    }
}

/// Public predicate evaluator for ad-hoc cohort queries.
///
/// Used by the query executor when `cohort_predicate` is set but no
/// materialized cohort exists.
#[must_use]
#[allow(clippy::implicit_hasher)]
pub fn evaluate_predicate(pred: &Predicate, metadata: &HashMap<String, String>) -> bool {
    eval_predicate(pred, metadata)
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::cohort::types::CohortDef;

    fn make_registry() -> Arc<CohortRegistry> {
        let reg = CohortRegistry::new();
        reg.define(CohortDef {
            name: "tech_en".into(),
            predicate: Predicate::And(vec![
                Predicate::Eq {
                    field: "locale".into(),
                    value: "en".into(),
                },
                Predicate::Eq {
                    field: "primary_category".into(),
                    value: "tech".into(),
                },
            ]),
        })
        .unwrap();
        reg.define(CohortDef {
            name: "sports".into(),
            predicate: Predicate::Eq {
                field: "primary_category".into(),
                value: "sports".into(),
            },
        })
        .unwrap();
        Arc::new(reg)
    }

    fn tech_user_metadata() -> HashMap<String, String> {
        let mut m = HashMap::new();
        m.insert("locale".into(), "en".into());
        m.insert("primary_category".into(), "tech".into());
        m
    }

    fn sports_user_metadata() -> HashMap<String, String> {
        let mut m = HashMap::new();
        m.insert("locale".into(), "en".into());
        m.insert("primary_category".into(), "sports".into());
        m
    }

    #[test]
    fn resolve_tech_user() {
        let reg = make_registry();
        let resolver = CohortResolver::new(reg);
        let memberships = resolver.resolve(1, &tech_user_metadata());
        let names: Vec<String> = memberships.iter().map(|n| n.to_string()).collect();
        assert!(names.contains(&"tech_en".to_string()));
        assert!(!names.contains(&"sports".to_string()));
    }

    #[test]
    fn resolve_sports_user() {
        let reg = make_registry();
        let resolver = CohortResolver::new(reg);
        let memberships = resolver.resolve(2, &sports_user_metadata());
        let names: Vec<String> = memberships.iter().map(|n| n.to_string()).collect();
        assert!(!names.contains(&"tech_en".to_string()));
        assert!(names.contains(&"sports".to_string()));
    }

    #[test]
    fn resolve_caches_result() {
        let reg = make_registry();
        let resolver = CohortResolver::new(reg);

        let m1 = resolver.resolve(1, &tech_user_metadata());
        assert_eq!(resolver.cache_len(), 1);

        // Second call should return cached result (we cannot directly observe
        // caching, but we can verify the result is identical).
        let m2 = resolver.resolve(1, &tech_user_metadata());
        assert_eq!(m1.len(), m2.len());
    }

    #[test]
    fn invalidate_clears_cache() {
        let reg = make_registry();
        let resolver = CohortResolver::new(reg);

        let _ = resolver.resolve(1, &tech_user_metadata());
        assert_eq!(resolver.cache_len(), 1);

        resolver.invalidate(1);
        assert_eq!(resolver.cache_len(), 0);
    }

    #[test]
    fn eval_eq_predicate() {
        let pred = Predicate::Eq {
            field: "locale".into(),
            value: "en".into(),
        };
        let mut meta = HashMap::new();
        meta.insert("locale".into(), "en".into());
        assert!(eval_predicate(&pred, &meta));
        meta.insert("locale".into(), "fr".into());
        assert!(!eval_predicate(&pred, &meta));
    }

    #[test]
    fn eval_any_predicate() {
        let pred = Predicate::Any {
            field: "locale".into(),
            values: vec!["en".into(), "fr".into()],
        };
        let mut meta = HashMap::new();
        meta.insert("locale".into(), "en".into());
        assert!(eval_predicate(&pred, &meta));
        meta.insert("locale".into(), "de".into());
        assert!(!eval_predicate(&pred, &meta));
    }

    #[test]
    fn eval_range_predicate() {
        let pred = Predicate::Range {
            field: "age".into(),
            lo: 18.0,
            hi: 35.0,
        };
        let mut meta = HashMap::new();
        meta.insert("age".into(), "25".into());
        assert!(eval_predicate(&pred, &meta));
        meta.insert("age".into(), "40".into());
        assert!(!eval_predicate(&pred, &meta));
        meta.insert("age".into(), "not_a_number".into());
        assert!(!eval_predicate(&pred, &meta));
    }

    #[test]
    fn eval_and_predicate() {
        let pred = Predicate::And(vec![
            Predicate::Eq {
                field: "a".into(),
                value: "1".into(),
            },
            Predicate::Eq {
                field: "b".into(),
                value: "2".into(),
            },
        ]);
        let mut meta = HashMap::new();
        meta.insert("a".into(), "1".into());
        meta.insert("b".into(), "2".into());
        assert!(eval_predicate(&pred, &meta));
        meta.insert("b".into(), "3".into());
        assert!(!eval_predicate(&pred, &meta));
    }

    #[test]
    fn eval_or_predicate() {
        let pred = Predicate::Or(vec![
            Predicate::Eq {
                field: "x".into(),
                value: "1".into(),
            },
            Predicate::Eq {
                field: "x".into(),
                value: "2".into(),
            },
        ]);
        let mut meta = HashMap::new();
        meta.insert("x".into(), "1".into());
        assert!(eval_predicate(&pred, &meta));
        meta.insert("x".into(), "3".into());
        assert!(!eval_predicate(&pred, &meta));
    }

    #[test]
    fn eval_missing_field_is_false() {
        let pred = Predicate::Eq {
            field: "missing".into(),
            value: "value".into(),
        };
        let meta = HashMap::new();
        assert!(!eval_predicate(&pred, &meta));
    }

    #[test]
    fn resolver_is_send_and_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<CohortResolver>();
    }
}
