//! Director-spawnable entity catalog: metadata for runtime browsing of
//! creatable entity types.
//!
//! `EntityCatalogEntry` is a pure data struct describing one spawnable
//! type (display name, default trait bitset, icon, tags). `EntityCatalog`
//! is a thin `Vec<EntityCatalogEntry>` wrapper with substring/trait/type
//! lookups.
//!
//! Both lived in `maat-engine` until PTM-1 phase B-prep; they are
//! data-only, so they belong in sdk-types where plugins can reference
//! them without pulling the engine. The legacy `maat_engine::catalog::*`
//! path remains as a re-export for the duration of the migration.

use crate::trait_data::TraitSet;
use crate::types::{AssetTag, EntityTypeId};

// ---------------------------------------------------------------------------
// EntityCatalogEntry — metadata for a spawnable entity type
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
pub struct EntityCatalogEntry {
    pub entity_type_id: EntityTypeId,
    pub display_name: String,
    pub description: String,
    pub default_traits: TraitSet,
    pub icon: Option<AssetTag>,
    /// Searchable tags: "creature", "spell", "condition", etc.
    pub tags: Vec<String>,
}

impl EntityCatalogEntry {
    pub fn new(
        entity_type_id: EntityTypeId,
        display_name: impl Into<String>,
        description: impl Into<String>,
        default_traits: TraitSet,
    ) -> Self {
        Self {
            entity_type_id,
            display_name: display_name.into(),
            description: description.into(),
            default_traits,
            icon: None,
            tags: vec![],
        }
    }

    pub fn with_icon(mut self, icon: AssetTag) -> Self {
        self.icon = Some(icon);
        self
    }

    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
        self.tags.push(tag.into());
        self
    }

    pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
        self.tags.extend(tags.into_iter().map(|t| t.into()));
        self
    }

    /// Returns true if the query string matches the display_name or any tag
    /// (case-insensitive substring match).
    pub fn matches_query(&self, query: &str) -> bool {
        let q = query.to_lowercase();
        if self.display_name.to_lowercase().contains(&q) {
            return true;
        }
        self.tags.iter().any(|t| t.to_lowercase().contains(&q))
    }
}

// ---------------------------------------------------------------------------
// EntityCatalog — runtime-browsable registry for director spawning
// ---------------------------------------------------------------------------

#[derive(Debug, Default)]
pub struct EntityCatalog {
    entries: Vec<EntityCatalogEntry>,
}

impl EntityCatalog {
    pub fn new() -> Self {
        Self { entries: vec![] }
    }

    pub fn register(&mut self, entry: EntityCatalogEntry) {
        self.entries.push(entry);
    }

    /// Simple substring match on display_name and tags (case-insensitive).
    pub fn search(&self, query: &str) -> Vec<&EntityCatalogEntry> {
        self.entries.iter().filter(|e| e.matches_query(query)).collect()
    }

    /// All entries whose `default_traits` contains all bits in `required`.
    pub fn by_trait(&self, required: TraitSet) -> Vec<&EntityCatalogEntry> {
        self.entries.iter()
            .filter(|e| e.default_traits.contains(required))
            .collect()
    }

    /// Look up by exact entity type ID.
    pub fn by_type(&self, type_id: EntityTypeId) -> Option<&EntityCatalogEntry> {
        self.entries.iter().find(|e| e.entity_type_id == type_id)
    }

    pub fn all(&self) -> &[EntityCatalogEntry] {
        &self.entries
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

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

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

    fn creature_entry() -> EntityCatalogEntry {
        EntityCatalogEntry::new(
            EntityTypeId::from_raw(1),
            "Goblin",
            "A sneaky goblin warrior",
            TraitSet::FEATURED | TraitSet::OWNABLE | TraitSet::TANGIBLE | TraitSet::MOVABLE | TraitSet::QUANTIFIED,
        )
        .with_tag("creature")
        .with_tag("enemy")
    }

    fn spell_entry() -> EntityCatalogEntry {
        EntityCatalogEntry::new(
            EntityTypeId::from_raw(2),
            "Fireball",
            "Explosive area-of-effect spell",
            TraitSet::FEATURED | TraitSet::ACTIVATABLE,
        )
        .with_tag("spell")
        .with_tag("damage")
        .with_icon(AssetTag::new("spell:fireball"))
    }

    fn condition_entry() -> EntityCatalogEntry {
        EntityCatalogEntry::new(
            EntityTypeId::from_raw(3),
            "Poisoned",
            "Deals damage each turn",
            TraitSet::QUANTIFIED,
        )
        .with_tag("condition")
        .with_tag("status")
    }

    #[test]
    fn register_and_all() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());
        catalog.register(spell_entry());
        assert_eq!(catalog.all().len(), 2);
        assert!(!catalog.is_empty());
    }

    #[test]
    fn search_by_display_name_substring() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());
        catalog.register(spell_entry());

        let results = catalog.search("gob");
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].display_name, "Goblin");
    }

    #[test]
    fn search_by_tag() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());
        catalog.register(spell_entry());
        catalog.register(condition_entry());

        let results = catalog.search("damage");
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].display_name, "Fireball");
    }

    #[test]
    fn search_case_insensitive() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());

        let results = catalog.search("CREATURE");
        assert_eq!(results.len(), 1);
    }

    #[test]
    fn search_no_match_returns_empty() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());

        let results = catalog.search("dragon");
        assert!(results.is_empty());
    }

    #[test]
    fn by_trait_exact_match() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());
        catalog.register(spell_entry());
        catalog.register(condition_entry());

        let results = catalog.by_trait(TraitSet::TANGIBLE);
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].display_name, "Goblin");
    }

    #[test]
    fn by_trait_activatable() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());
        catalog.register(spell_entry());

        let results = catalog.by_trait(TraitSet::ACTIVATABLE);
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].display_name, "Fireball");
    }

    #[test]
    fn by_trait_compound_filter() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());
        catalog.register(spell_entry());

        let results = catalog.by_trait(TraitSet::FEATURED | TraitSet::TANGIBLE);
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].display_name, "Goblin");
    }

    #[test]
    fn by_type_found() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());
        catalog.register(spell_entry());

        let entry = catalog.by_type(EntityTypeId::from_raw(2));
        assert!(entry.is_some());
        assert_eq!(entry.unwrap().display_name, "Fireball");
    }

    #[test]
    fn by_type_not_found() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());

        let entry = catalog.by_type(EntityTypeId::from_raw(999));
        assert!(entry.is_none());
    }

    #[test]
    fn empty_catalog() {
        let catalog = EntityCatalog::new();
        assert!(catalog.is_empty());
        assert_eq!(catalog.len(), 0);
        assert!(catalog.search("anything").is_empty());
        assert!(catalog.by_trait(TraitSet::FEATURED).is_empty());
        assert!(catalog.by_type(EntityTypeId::from_raw(1)).is_none());
    }

    #[test]
    fn entry_with_icon() {
        let entry = spell_entry();
        assert!(entry.icon.is_some());
        assert_eq!(entry.icon.as_ref().unwrap().name, "spell:fireball");
    }

    #[test]
    fn multiple_tags_all_searchable() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());

        assert_eq!(catalog.search("creature").len(), 1);
        assert_eq!(catalog.search("enemy").len(), 1);
    }

    #[test]
    fn search_multiple_matches() {
        let mut catalog = EntityCatalog::new();
        catalog.register(creature_entry());
        catalog.register(
            EntityCatalogEntry::new(
                EntityTypeId::from_raw(4),
                "Orc",
                "A brutal orc warrior",
                TraitSet::FEATURED | TraitSet::OWNABLE | TraitSet::TANGIBLE | TraitSet::MOVABLE,
            )
            .with_tag("creature")
        );

        let results = catalog.search("creature");
        assert_eq!(results.len(), 2);
    }
}
