use indexmap::IndexMap;
use serde::{Deserialize, Serialize};

pub type CategoryId = usize;
pub type ItemId = usize;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
    pub id: ItemId,
    pub name: String,
    /// Parent group name, if any
    pub group: Option<String>,
}

impl Item {
    pub fn new(id: ItemId, name: impl Into<String>) -> Self {
        Self {
            id,
            name: name.into(),
            group: None,
        }
    }

    pub fn with_group(mut self, group: impl Into<String>) -> Self {
        self.group = Some(group.into());
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Group {
    pub name: String,
    /// Parent group name for nested hierarchies
    pub parent: Option<String>,
}

impl Group {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            parent: None,
        }
    }

    pub fn with_parent(mut self, parent: impl Into<String>) -> Self {
        self.parent = Some(parent.into());
        self
    }
}

/// What kind of category this is.
/// Regular categories store their items explicitly. Virtual categories
/// are synthesized at query time by the layout layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum CategoryKind {
    #[default]
    Regular,
    /// Items are "0", "1", ... N where N = number of matching cells.
    VirtualIndex,
    /// Items are the names of all regular categories + "Value".
    VirtualDim,
    /// The measure dimension. Items come from two sources: numeric data
    /// fields (listed in the file) and formula targets (added automatically
    /// by add_formula). Virtual because formula-derived items are implied
    /// by the formula definitions — listing them explicitly would be
    /// redundant in the file format and confusing in the UI.
    VirtualMeasure,
    /// High-cardinality per-row field (description, id, note). Stored
    /// alongside the data so it shows up in record/drill views, but
    /// defaults to Axis::None and is excluded from pivot limits and the
    /// auto Row/Column axis assignment.
    Label,
}

impl CategoryKind {
    /// True for user-managed pivot dimensions (what the category
    /// count limit and auto axis assignment apply to).
    pub fn is_regular(&self) -> bool {
        matches!(self, CategoryKind::Regular)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Category {
    pub id: CategoryId,
    pub name: String,
    /// Items in insertion order
    pub items: IndexMap<String, Item>,
    /// Named groups (hierarchy nodes)
    pub groups: Vec<Group>,
    /// Next item id counter
    next_item_id: ItemId,
    /// Whether this is a regular or virtual category
    #[serde(default)]
    pub kind: CategoryKind,
}

impl Category {
    pub fn new(id: CategoryId, name: impl Into<String>) -> Self {
        Self {
            id,
            name: name.into(),
            items: IndexMap::new(),
            groups: Vec::new(),
            next_item_id: 0,
            kind: CategoryKind::Regular,
        }
    }

    pub fn with_kind(mut self, kind: CategoryKind) -> Self {
        self.kind = kind;
        self
    }

    pub fn add_item(&mut self, name: impl Into<String>) -> ItemId {
        let name = name.into();
        if let Some(item) = self.items.get(&name) {
            return item.id;
        }
        let id = self.next_item_id;
        self.next_item_id += 1;
        self.items.insert(name.clone(), Item::new(id, name));
        id
    }

    pub fn remove_item(&mut self, name: &str) {
        self.items.shift_remove(name);
    }

    pub fn add_item_in_group(
        &mut self,
        name: impl Into<String>,
        group: impl Into<String>,
    ) -> ItemId {
        let name = name.into();
        let group = group.into();
        if let Some(item) = self.items.get(&name) {
            return item.id;
        }
        let id = self.next_item_id;
        self.next_item_id += 1;
        self.items
            .insert(name.clone(), Item::new(id, name).with_group(group));
        id
    }

    pub fn add_group(&mut self, group: Group) {
        if !self.groups.iter().any(|g| g.name == group.name) {
            self.groups.push(group);
        }
    }

    /// Returns item names in order, grouped hierarchically
    pub fn ordered_item_names(&self) -> Vec<&str> {
        self.items.keys().map(|s| s.as_str()).collect()
    }
}

#[cfg(test)]
mod tests {
    use super::{Category, Group};

    fn cat() -> Category {
        Category::new(0, "Region")
    }

    #[test]
    fn add_item_returns_sequential_ids() {
        let mut c = cat();
        let id0 = c.add_item("East");
        let id1 = c.add_item("West");
        assert_eq!(id0, 0);
        assert_eq!(id1, 1);
    }

    #[test]
    fn add_item_duplicate_returns_same_id() {
        let mut c = cat();
        let id1 = c.add_item("East");
        let id2 = c.add_item("East");
        assert_eq!(id1, id2);
        assert_eq!(c.items.len(), 1);
    }

    #[test]
    fn add_item_in_group_sets_group() {
        let mut c = cat();
        c.add_item_in_group("Jan", "Q1");
        assert_eq!(
            c.items.get("Jan").and_then(|i| i.group.as_deref()),
            Some("Q1")
        );
    }

    #[test]
    fn add_item_in_group_duplicate_returns_same_id() {
        let mut c = cat();
        let id1 = c.add_item_in_group("Jan", "Q1");
        let id2 = c.add_item_in_group("Jan", "Q1");
        assert_eq!(id1, id2);
        assert_eq!(c.items.len(), 1);
    }

    #[test]
    fn add_group_deduplicates() {
        let mut c = cat();
        c.add_group(Group::new("Q1"));
        c.add_group(Group::new("Q1"));
        assert_eq!(c.groups.len(), 1);
    }

    #[test]
    fn item_index_reflects_insertion_order() {
        let mut c = cat();
        c.add_item("East");
        c.add_item("West");
        c.add_item("North");
        assert_eq!(c.items.get_index_of("East"), Some(0));
        assert_eq!(c.items.get_index_of("West"), Some(1));
        assert_eq!(c.items.get_index_of("North"), Some(2));
    }
}
