use crate::investigation_pack::InvestigationPack;
use ratatui::widgets::TableState;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct InvestigationsModel {
pub packs: Vec<InvestigationEntry>,
pub table_state: TableState,
pub loading: bool,
pub error: Option<String>,
pub active: Option<ActiveInvestigation>,
pub input_collection: Option<InputCollectionState>,
}
#[derive(Debug, Clone)]
pub struct InvestigationEntry {
pub path: PathBuf,
pub pack: Option<InvestigationPack>,
pub relative_path: String,
pub load_error: Option<String>,
pub validation_error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct InputCollectionState {
pub pack_path: PathBuf,
pub inputs: HashMap<String, String>,
pub current_input: usize,
pub current_value: String,
pub input_names: Vec<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ActiveInvestigation {
pub name: String,
pub pack_path: PathBuf,
pub steps: Vec<StepProgress>,
pub workspace_progress: HashMap<String, WorkspaceProgress>,
pub status: InvestigationStatus,
pub output_folder: Option<PathBuf>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct StepProgress {
pub name: String,
pub depends_on: Vec<String>,
pub status: StepState,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum StepState {
Pending,
Running,
Completed { rows: usize },
Failed { error: String },
Skipped,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct WorkspaceProgress {
pub name: String,
pub current_step: Option<String>,
pub completed_steps: usize,
pub total_steps: usize,
pub status: InvestigationStatus,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum InvestigationStatus {
Pending,
Running,
Completed,
Failed,
}
impl InvestigationsModel {
pub fn new() -> Self {
Self {
packs: Vec::new(),
table_state: TableState::default(),
loading: false,
error: None,
active: None,
input_collection: None,
}
}
pub fn refresh(&mut self) {
self.loading = true;
self.error = None;
match self.load_packs_from_library() {
Ok(packs) => {
self.packs = packs;
if !self.packs.is_empty() && self.table_state.selected().is_none() {
self.table_state.select(Some(0));
}
}
Err(e) => {
self.error = Some(format!("Failed to load investigations: {}", e));
}
}
self.loading = false;
}
fn load_packs_from_library(&self) -> crate::error::Result<Vec<InvestigationEntry>> {
let pack_paths = InvestigationPack::list_library_packs()?;
let library_root = InvestigationPack::get_library_path("")?;
let mut entries = Vec::new();
for path in pack_paths {
let relative_path = path
.strip_prefix(&library_root)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
entries.push(InvestigationEntry {
path: path.clone(),
pack: None,
relative_path,
load_error: None,
validation_error: None,
});
}
entries.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
Ok(entries)
}
pub fn get_selected_entry(&self) -> Option<&InvestigationEntry> {
self.table_state.selected().and_then(|i| self.packs.get(i))
}
pub fn get_selected_entry_mut(&mut self) -> Option<&mut InvestigationEntry> {
self.table_state
.selected()
.and_then(|i| self.packs.get_mut(i))
}
pub fn load_selected_pack(&mut self) -> crate::error::Result<()> {
if let Some(entry) = self.get_selected_entry_mut() {
if entry.pack.is_none() && entry.load_error.is_none() {
match InvestigationPack::load_from_file(&entry.path) {
Ok(pack) => {
if let Err(e) = pack.validate() {
entry.validation_error = Some(e.to_string());
}
entry.pack = Some(pack);
}
Err(e) => {
entry.load_error = Some(format!("Parse error: {}", e));
return Err(e);
}
}
}
}
Ok(())
}
pub fn previous(&mut self) {
if self.packs.is_empty() {
return;
}
let i = match self.table_state.selected() {
Some(i) => {
if i == 0 {
self.packs.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.table_state.select(Some(i));
}
pub fn next(&mut self) {
if self.packs.is_empty() {
return;
}
let i = match self.table_state.selected() {
Some(i) => {
if i >= self.packs.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.table_state.select(Some(i));
}
pub fn pack_count(&self) -> usize {
self.packs.len()
}
pub fn start_input_collection(&mut self) -> Option<()> {
let entry = self.get_selected_entry()?;
let pack = entry.pack.as_ref()?;
if pack.inputs.is_empty() {
return None; }
let input_names: Vec<String> = pack.inputs.iter().map(|i| i.name.clone()).collect();
let mut inputs = HashMap::new();
for input in &pack.inputs {
if let Some(default) = &input.default {
inputs.insert(input.name.clone(), default.clone());
}
}
self.input_collection = Some(InputCollectionState {
pack_path: entry.path.clone(),
inputs,
current_input: 0,
current_value: String::new(),
input_names,
});
if let Some(state) = &mut self.input_collection {
if let Some(first_name) = state.input_names.first() {
if let Some(default) = state.inputs.get(first_name) {
state.current_value = default.clone();
}
}
}
Some(())
}
pub fn next_input(&mut self) {
if let Some(state) = &mut self.input_collection {
if let Some(name) = state.input_names.get(state.current_input) {
state.inputs.insert(name.clone(), state.current_value.clone());
}
if state.current_input < state.input_names.len().saturating_sub(1) {
state.current_input += 1;
if let Some(name) = state.input_names.get(state.current_input) {
state.current_value = state.inputs.get(name).cloned().unwrap_or_default();
}
}
}
}
pub fn prev_input(&mut self) {
if let Some(state) = &mut self.input_collection {
if let Some(name) = state.input_names.get(state.current_input) {
state.inputs.insert(name.clone(), state.current_value.clone());
}
if state.current_input > 0 {
state.current_input -= 1;
if let Some(name) = state.input_names.get(state.current_input) {
state.current_value = state.inputs.get(name).cloned().unwrap_or_default();
}
}
}
}
pub fn finalize_inputs(&mut self) -> Option<HashMap<String, String>> {
if let Some(mut state) = self.input_collection.take() {
if let Some(name) = state.input_names.get(state.current_input) {
state.inputs.insert(name.clone(), state.current_value.clone());
}
Some(state.inputs)
} else {
None
}
}
pub fn cancel_input_collection(&mut self) {
self.input_collection = None;
}
pub fn is_collecting_inputs(&self) -> bool {
self.input_collection.is_some()
}
}
impl Default for InvestigationsModel {
fn default() -> Self {
Self::new()
}
}
impl InvestigationEntry {
pub fn get_display_name(&self) -> String {
if let Some(pack) = &self.pack {
pack.name.clone()
} else {
self.path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Unknown")
.to_string()
}
}
#[allow(dead_code)]
pub fn get_description(&self) -> Option<&str> {
self.pack.as_ref()?.description.as_deref()
}
pub fn get_step_count(&self) -> Option<usize> {
self.pack.as_ref().map(|p| p.steps.len())
}
pub fn get_input_count(&self) -> Option<usize> {
self.pack.as_ref().map(|p| p.inputs.len())
}
pub fn has_error(&self) -> bool {
self.load_error.is_some() || self.validation_error.is_some()
}
pub fn get_error(&self) -> Option<&str> {
self.load_error
.as_deref()
.or(self.validation_error.as_deref())
}
}