//! A [`Workbook`] wraps a pure-data [`Model`] with the set of named [`View`]s
//! that are rendered over it. Splitting the two breaks the former
//! `Model ↔ View` cycle: `Model` knows nothing about views, while `View`
//! depends on `Model` (one direction).
//!
//! Cross-slice operations — adding or removing a category, for example, must
//! update both the model's categories and every view's axis assignments
//! — live here rather than on `Model`, so `Model` stays pure data and
//! `improvise-core` can be extracted without pulling view code along.

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

use crate::model::Model;
use crate::model::category::CategoryId;
use crate::view::{Axis, View};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workbook {
    pub model: Model,
    pub views: IndexMap<String, View>,
    pub active_view: String,
}

impl Workbook {
    /// Create a new workbook with a fresh `Model` and a single `Default` view.
    /// Virtual categories (`_Index`, `_Dim`, `_Measure`) are registered on the
    /// default view. All virtuals default to `Axis::None` via
    /// `on_category_added` (see improvise-709f2df), then `_Measure` is bumped
    /// to `Axis::Page` so aggregated pivot views show a single measure at a
    /// time (see improvise-kos). Leaving `_Index`/`_Dim` on None keeps pivot
    /// mode the default — records mode activates only when the user moves
    /// both onto axes.
    pub fn new(name: impl Into<String>) -> Self {
        let model = Model::new(name);
        let mut views = IndexMap::new();
        views.insert("Default".to_string(), View::new("Default"));
        let mut wb = Self {
            model,
            views,
            active_view: "Default".to_string(),
        };
        for view in wb.views.values_mut() {
            for cat_name in wb.model.categories.keys() {
                view.on_category_added(cat_name);
            }
            view.set_axis("_Measure", Axis::Page);
        }
        wb
    }

    // ── Cross-slice category management ─────────────────────────────────────

    /// Add a regular pivot category and register it with every view.
    pub fn add_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
        let name = name.into();
        let id = self.model.add_category(&name)?;
        for view in self.views.values_mut() {
            view.on_category_added(&name);
        }
        Ok(id)
    }

    /// Add a label category (excluded from pivot-count limit) and register it
    /// with every view on `Axis::None`.
    pub fn add_label_category(&mut self, name: impl Into<String>) -> Result<CategoryId> {
        let name = name.into();
        let id = self.model.add_label_category(&name)?;
        for view in self.views.values_mut() {
            view.on_category_added(&name);
            view.set_axis(&name, Axis::None);
        }
        Ok(id)
    }

    /// Remove a category from the model and from every view.
    pub fn remove_category(&mut self, name: &str) {
        self.model.remove_category(name);
        for view in self.views.values_mut() {
            view.on_category_removed(name);
        }
    }

    // ── Active view access ──────────────────────────────────────────────────

    pub fn active_view(&self) -> &View {
        self.views
            .get(&self.active_view)
            .expect("active_view always names an existing view")
    }

    pub fn active_view_mut(&mut self) -> &mut View {
        self.views
            .get_mut(&self.active_view)
            .expect("active_view always names an existing view")
    }

    // ── View management ─────────────────────────────────────────────────────

    /// Create a new view pre-populated with every existing category, and
    /// return a mutable reference to it. Does not change the active view.
    pub fn create_view(&mut self, name: impl Into<String>) -> &mut View {
        let name = name.into();
        let mut view = View::new(name.clone());
        for cat_name in self.model.categories.keys() {
            view.on_category_added(cat_name);
        }
        self.views.insert(name.clone(), view);
        self.views.get_mut(&name).unwrap()
    }

    pub fn switch_view(&mut self, name: &str) -> Result<()> {
        if self.views.contains_key(name) {
            self.active_view = name.to_string();
            Ok(())
        } else {
            Err(anyhow!("View '{name}' not found"))
        }
    }

    pub fn delete_view(&mut self, name: &str) -> Result<()> {
        if self.views.len() <= 1 {
            return Err(anyhow!("Cannot delete the last view"));
        }
        self.views.shift_remove(name);
        if self.active_view == name {
            self.active_view = self.views.keys().next().unwrap().clone();
        }
        Ok(())
    }

    /// Reset all view scroll offsets to zero. Call after loading or replacing
    /// a workbook so stale offsets don't render an empty grid.
    pub fn normalize_view_state(&mut self) {
        for view in self.views.values_mut() {
            view.row_offset = 0;
            view.col_offset = 0;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Workbook;
    use crate::view::Axis;

    #[test]
    fn new_workbook_has_default_view_with_virtuals_seeded() {
        let wb = Workbook::new("Test");
        assert_eq!(wb.active_view, "Default");
        let v = wb.active_view();
        // Virtual categories default to Axis::None; _Measure is bumped to Page
        // so aggregated pivot views show a single measure by default
        // (improvise-kos, improvise-709f2df).
        assert_eq!(v.axis_of("_Index"), Axis::None);
        assert_eq!(v.axis_of("_Dim"), Axis::None);
        assert_eq!(v.axis_of("_Measure"), Axis::Page);
    }

    #[test]
    fn add_category_notifies_all_views() {
        let mut wb = Workbook::new("Test");
        wb.create_view("Secondary");
        wb.add_category("Region").unwrap();
        // Both views should know about Region (axis_of panics on unknown).
        let _ = wb.views.get("Default").unwrap().axis_of("Region");
        let _ = wb.views.get("Secondary").unwrap().axis_of("Region");
    }

    #[test]
    fn add_label_category_sets_none_axis_on_all_views() {
        let mut wb = Workbook::new("Test");
        wb.create_view("Other");
        wb.add_label_category("Note").unwrap();
        assert_eq!(wb.views.get("Default").unwrap().axis_of("Note"), Axis::None);
        assert_eq!(wb.views.get("Other").unwrap().axis_of("Note"), Axis::None);
    }

    #[test]
    fn remove_category_removes_from_all_views() {
        let mut wb = Workbook::new("Test");
        wb.add_category("Region").unwrap();
        wb.create_view("Second");
        wb.remove_category("Region");
        // Region should no longer appear in either view's Row axis.
        assert!(
            wb.views
                .get("Default")
                .unwrap()
                .categories_on(Axis::Row)
                .iter()
                .all(|c| *c != "Region")
        );
        assert!(
            wb.views
                .get("Second")
                .unwrap()
                .categories_on(Axis::Row)
                .iter()
                .all(|c| *c != "Region")
        );
    }

    #[test]
    fn switch_view_changes_active_view() {
        let mut wb = Workbook::new("Test");
        wb.create_view("Other");
        wb.switch_view("Other").unwrap();
        assert_eq!(wb.active_view, "Other");
    }

    #[test]
    fn switch_view_unknown_returns_error() {
        let mut wb = Workbook::new("Test");
        assert!(wb.switch_view("NoSuchView").is_err());
    }

    #[test]
    fn delete_view_removes_it() {
        let mut wb = Workbook::new("Test");
        wb.create_view("Extra");
        wb.delete_view("Extra").unwrap();
        assert!(!wb.views.contains_key("Extra"));
    }

    #[test]
    fn delete_last_view_returns_error() {
        let wb = Workbook::new("Test");
        // Use wb without binding mut — delete_view would need &mut, so:
        let mut wb = wb;
        assert!(wb.delete_view("Default").is_err());
    }

    #[test]
    fn delete_active_view_switches_to_another() {
        let mut wb = Workbook::new("Test");
        wb.create_view("Other");
        wb.switch_view("Other").unwrap();
        wb.delete_view("Other").unwrap();
        assert_ne!(wb.active_view, "Other");
    }

    #[test]
    fn first_category_goes_to_row_second_to_column_rest_to_page() {
        let mut wb = Workbook::new("Test");
        wb.add_category("Region").unwrap();
        wb.add_category("Product").unwrap();
        wb.add_category("Time").unwrap();
        let v = wb.active_view();
        assert_eq!(v.axis_of("Region"), Axis::Row);
        assert_eq!(v.axis_of("Product"), Axis::Column);
        assert_eq!(v.axis_of("Time"), Axis::Page);
    }

    #[test]
    fn create_view_copies_category_structure() {
        let mut wb = Workbook::new("Test");
        wb.add_category("Region").unwrap();
        wb.add_category("Product").unwrap();
        wb.create_view("Secondary");
        let v = wb.views.get("Secondary").unwrap();
        let _ = v.axis_of("Region");
        let _ = v.axis_of("Product");
    }
}
