use super::types::{CellValueArg, Command, CommandResult}; use crate::formula::parse_formula; use crate::import::analyzer::{analyze_records, extract_array_at_path, FieldKind}; use crate::model::cell::{CellKey, CellValue}; use crate::model::Model; use crate::persistence; /// Execute a command against the model, returning a result. /// This is the single authoritative mutation path used by both the TUI and headless modes. pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult { match cmd { Command::AddCategory { name } => match model.add_category(name) { Ok(_) => CommandResult::ok_msg(format!("Category '{name}' added")), Err(e) => CommandResult::err(e.to_string()), }, Command::AddItem { category, item } => match model.category_mut(category) { Some(cat) => { cat.add_item(item); CommandResult::ok() } None => CommandResult::err(format!("Category '{category}' not found")), }, Command::AddItemInGroup { category, item, group, } => match model.category_mut(category) { Some(cat) => { cat.add_item_in_group(item, group); CommandResult::ok() } None => CommandResult::err(format!("Category '{category}' not found")), }, Command::SetCell { coords, value } => { let kv: Vec<(String, String)> = coords .iter() .map(|pair| (pair[0].clone(), pair[1].clone())) .collect(); // Validate all categories exist before mutating anything for (cat_name, _) in &kv { if model.category(cat_name).is_none() { return CommandResult::err(format!("Category '{cat_name}' not found")); } } // Ensure items exist within their categories for (cat_name, item_name) in &kv { model.category_mut(cat_name).unwrap().add_item(item_name); } let key = CellKey::new(kv); let cell_value = match value { CellValueArg::Number { number } => CellValue::Number(*number), CellValueArg::Text { text } => CellValue::Text(text.clone()), }; model.set_cell(key, cell_value); CommandResult::ok() } Command::ClearCell { coords } => { let kv: Vec<(String, String)> = coords .iter() .map(|pair| (pair[0].clone(), pair[1].clone())) .collect(); let key = CellKey::new(kv); model.clear_cell(&key); CommandResult::ok() } Command::AddFormula { raw, target_category, } => { match parse_formula(raw, target_category) { Ok(formula) => { // Ensure the target item exists in the target category let target = formula.target.clone(); let cat_name = formula.target_category.clone(); if let Some(cat) = model.category_mut(&cat_name) { cat.add_item(&target); } model.add_formula(formula); CommandResult::ok_msg(format!("Formula '{raw}' added")) } Err(e) => CommandResult::err(format!("Parse error: {e}")), } } Command::RemoveFormula { target, target_category, } => { model.remove_formula(target, target_category); CommandResult::ok() } Command::CreateView { name } => { model.create_view(name); CommandResult::ok() } Command::DeleteView { name } => match model.delete_view(name) { Ok(_) => CommandResult::ok(), Err(e) => CommandResult::err(e.to_string()), }, Command::SwitchView { name } => match model.switch_view(name) { Ok(_) => CommandResult::ok(), Err(e) => CommandResult::err(e.to_string()), }, Command::SetAxis { category, axis } => { model.active_view_mut().set_axis(category, *axis); CommandResult::ok() } Command::SetPageSelection { category, item } => { model.active_view_mut().set_page_selection(category, item); CommandResult::ok() } Command::ToggleGroup { category, group } => { model .active_view_mut() .toggle_group_collapse(category, group); CommandResult::ok() } Command::HideItem { category, item } => { model.active_view_mut().hide_item(category, item); CommandResult::ok() } Command::ShowItem { category, item } => { model.active_view_mut().show_item(category, item); CommandResult::ok() } Command::Save { path } => match persistence::save(model, std::path::Path::new(path)) { Ok(_) => CommandResult::ok_msg(format!("Saved to {path}")), Err(e) => CommandResult::err(e.to_string()), }, Command::Load { path } => match persistence::load(std::path::Path::new(path)) { Ok(mut loaded) => { loaded.normalize_view_state(); *model = loaded; CommandResult::ok_msg(format!("Loaded from {path}")) } Err(e) => CommandResult::err(e.to_string()), }, Command::ExportCsv { path } => { let view_name = model.active_view.clone(); match persistence::export_csv(model, &view_name, std::path::Path::new(path)) { Ok(_) => CommandResult::ok_msg(format!("Exported to {path}")), Err(e) => CommandResult::err(e.to_string()), } } Command::ImportJson { path, model_name, array_path, } => import_json_headless(model, path, model_name.as_deref(), array_path.as_deref()), } } fn import_json_headless( model: &mut Model, path: &str, model_name: Option<&str>, array_path: Option<&str>, ) -> CommandResult { let content = match std::fs::read_to_string(path) { Ok(c) => c, Err(e) => return CommandResult::err(format!("Cannot read '{path}': {e}")), }; let value: serde_json::Value = match serde_json::from_str(&content) { Ok(v) => v, Err(e) => return CommandResult::err(format!("JSON parse error: {e}")), }; let records = if let Some(ap) = array_path.filter(|s| !s.is_empty()) { match extract_array_at_path(&value, ap) { Some(arr) => arr.clone(), None => return CommandResult::err(format!("No array at path '{ap}'")), } } else if let Some(arr) = value.as_array() { arr.clone() } else { // Find first array let paths = crate::import::analyzer::find_array_paths(&value); if let Some(first) = paths.first() { match extract_array_at_path(&value, first) { Some(arr) => arr.clone(), None => return CommandResult::err("Could not extract records array"), } } else { return CommandResult::err("No array found in JSON"); } }; let proposals = analyze_records(&records); // Auto-accept all and build via ImportPipeline let pipeline = crate::import::wizard::ImportPipeline { raw: value, array_paths: vec![], selected_path: array_path.unwrap_or("").to_string(), records, proposals: proposals .into_iter() .map(|mut p| { p.accepted = p.kind != FieldKind::Label; p }) .collect(), model_name: model_name.unwrap_or("Imported Model").to_string(), }; match pipeline.build_model() { Ok(new_model) => { *model = new_model; CommandResult::ok_msg("JSON imported successfully") } Err(e) => CommandResult::err(e.to_string()), } }