//! Cohort definition types: predicates, definitions, and the registry.
//!
//! A cohort is a named group of users defined by a predicate evaluated against
//! user metadata. The `CohortRegistry` stores all defined cohorts and provides
//! concurrent access via `DashMap`.

use std::sync::Arc;

use dashmap::DashMap;
use serde::{Deserialize, Serialize};

use crate::schema::TidalError;

/// Cheap-to-clone cohort name handle.
pub type CohortName = Arc<str>;

/// A predicate evaluated against user metadata `HashMap<String, String>`.
///
/// Predicates compose via `And` and `Or` for arbitrary boolean expressions
/// over user attributes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Predicate {
    /// Exact match: `metadata[field] == value`.
    Eq { field: String, value: String },
    /// Set membership: `metadata[field]` is one of `values`.
    Any { field: String, values: Vec<String> },
    /// Numeric range (inclusive): `lo <= metadata[field].parse::<f64>() <= hi`.
    Range { field: String, lo: f64, hi: f64 },
    /// All sub-predicates must be true.
    And(Vec<Predicate>),
    /// At least one sub-predicate must be true.
    Or(Vec<Predicate>),
}

/// A named cohort definition consisting of a name and a predicate.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CohortDef {
    /// Human-readable cohort name (e.g., `"tech_enthusiasts"`).
    pub name: String,
    /// Predicate that determines cohort membership.
    pub predicate: Predicate,
}

/// Thread-safe registry of all defined cohorts.
///
/// Uses `DashMap` for concurrent reads/writes without mutex contention
/// on the hot path.
pub struct CohortRegistry {
    cohorts: DashMap<CohortName, CohortDef>,
}

impl CohortRegistry {
    /// Create an empty registry.
    #[must_use]
    pub fn new() -> Self {
        Self {
            cohorts: DashMap::new(),
        }
    }

    /// Register a cohort definition.
    ///
    /// # Errors
    ///
    /// Returns `TidalError::Internal` if a cohort with the same name already exists.
    pub fn define(&self, def: CohortDef) -> Result<(), TidalError> {
        let name: CohortName = Arc::from(def.name.as_str());
        if self.cohorts.contains_key(&name) {
            return Err(TidalError::internal(
                "define_cohort",
                format!("cohort '{}' already defined", def.name),
            ));
        }
        self.cohorts.insert(name, def);
        Ok(())
    }

    /// Look up a cohort by name.
    ///
    /// Returns a `DashMap` reference guard or `None` if not found.
    #[must_use]
    pub fn get(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, CohortName, CohortDef>> {
        let key: CohortName = Arc::from(name);
        self.cohorts.get(&key)
    }

    /// List all registered cohort names.
    #[must_use]
    pub fn names(&self) -> Vec<CohortName> {
        self.cohorts.iter().map(|r| r.key().clone()).collect()
    }

    /// Number of registered cohorts.
    #[must_use]
    pub fn len(&self) -> usize {
        self.cohorts.len()
    }

    /// Whether the registry is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.cohorts.is_empty()
    }
}

impl Default for CohortRegistry {
    fn default() -> Self {
        Self::new()
    }
}

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

// ── Serialization ─────────────────────────────────────────────────────────────

/// Serialize a cohort definition as JSON bytes for durable storage.
#[must_use]
pub fn serialize_cohort_def(def: &CohortDef) -> Vec<u8> {
    // serde_json::to_vec will not fail for these types (no maps with
    // non-string keys, no recursion limits exceeded). Falling back to
    // an empty vec is defensive but should never happen.
    serde_json::to_vec(def).unwrap_or_default()
}

/// Deserialize a cohort definition from JSON bytes.
///
/// Returns `None` on malformed input -- never panics.
#[must_use]
pub fn deserialize_cohort_def(bytes: &[u8]) -> Option<CohortDef> {
    serde_json::from_slice(bytes).ok()
}

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

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

    fn tech_predicate() -> Predicate {
        Predicate::And(vec![
            Predicate::Eq {
                field: "locale".into(),
                value: "en".into(),
            },
            Predicate::Eq {
                field: "primary_category".into(),
                value: "tech".into(),
            },
        ])
    }

    #[test]
    fn define_and_get() {
        let registry = CohortRegistry::new();
        let def = CohortDef {
            name: "tech".into(),
            predicate: tech_predicate(),
        };
        registry.define(def).unwrap();
        assert!(registry.get("tech").is_some());
        assert!(registry.get("nonexistent").is_none());
    }

    #[test]
    fn define_duplicate_fails() {
        let registry = CohortRegistry::new();
        let def1 = CohortDef {
            name: "tech".into(),
            predicate: tech_predicate(),
        };
        let def2 = CohortDef {
            name: "tech".into(),
            predicate: tech_predicate(),
        };
        registry.define(def1).unwrap();
        assert!(registry.define(def2).is_err());
    }

    #[test]
    fn names_and_len() {
        let registry = CohortRegistry::new();
        assert!(registry.is_empty());
        assert_eq!(registry.len(), 0);

        registry
            .define(CohortDef {
                name: "a".into(),
                predicate: Predicate::Eq {
                    field: "x".into(),
                    value: "1".into(),
                },
            })
            .unwrap();
        registry
            .define(CohortDef {
                name: "b".into(),
                predicate: Predicate::Eq {
                    field: "x".into(),
                    value: "2".into(),
                },
            })
            .unwrap();

        assert_eq!(registry.len(), 2);
        assert!(!registry.is_empty());

        let mut names: Vec<String> = registry.names().iter().map(|n| n.to_string()).collect();
        names.sort();
        assert_eq!(names, vec!["a", "b"]);
    }

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

    #[test]
    fn serde_roundtrip_simple() {
        let def = CohortDef {
            name: "tech_en".into(),
            predicate: Predicate::Eq {
                field: "locale".into(),
                value: "en".into(),
            },
        };
        let bytes = serialize_cohort_def(&def);
        let restored = deserialize_cohort_def(&bytes).unwrap();
        assert_eq!(restored.name, "tech_en");
        match &restored.predicate {
            Predicate::Eq { field, value } => {
                assert_eq!(field, "locale");
                assert_eq!(value, "en");
            }
            other => panic!("expected Eq, got {other:?}"),
        }
    }

    #[test]
    fn serde_roundtrip_compound() {
        let def = CohortDef {
            name: "compound".into(),
            predicate: Predicate::And(vec![
                Predicate::Eq {
                    field: "locale".into(),
                    value: "en".into(),
                },
                Predicate::Or(vec![
                    Predicate::Any {
                        field: "interest".into(),
                        values: vec!["tech".into(), "science".into()],
                    },
                    Predicate::Range {
                        field: "age".into(),
                        lo: 18.0,
                        hi: 65.0,
                    },
                ]),
            ]),
        };
        let bytes = serialize_cohort_def(&def);
        let restored = deserialize_cohort_def(&bytes).unwrap();
        assert_eq!(restored.name, "compound");
        // Verify the structure survived.
        match &restored.predicate {
            Predicate::And(parts) => assert_eq!(parts.len(), 2),
            other => panic!("expected And, got {other:?}"),
        }
    }

    #[test]
    fn deserialize_malformed_returns_none() {
        assert!(deserialize_cohort_def(b"").is_none());
        assert!(deserialize_cohort_def(b"not json").is_none());
        assert!(deserialize_cohort_def(b"{}").is_none());
    }
}
