use anyhow::Result;
use colored::Colorize;
use std::collections::HashMap;
use std::path::PathBuf;
use crate::models::task::{Task, TaskStatus};
use crate::storage::Storage;
pub fn run(project_root: Option<PathBuf>, dry_run: bool) -> Result<()> {
let storage = Storage::new(project_root);
let tasks_file = storage.tasks_file();
if !tasks_file.exists() {
println!("{}", "No tasks file found. Nothing to migrate.".yellow());
return Ok(());
}
let mut all_tasks = storage.load_tasks()?;
let mut changes: Vec<String> = Vec::new();
let mut parent_fixes = 0;
let mut subtask_links = 0;
for (epic_tag, epic) in all_tasks.iter_mut() {
let mut id_map: HashMap<String, String> = HashMap::new();
for task in &epic.tasks {
if !task.id.contains(':') {
let new_id = Task::make_id(epic_tag, &task.id);
id_map.insert(task.id.clone(), new_id.clone());
changes.push(format!("{}: {} -> {}", epic_tag, task.id, new_id));
}
}
for task in &mut epic.tasks {
if let Some(new_id) = id_map.get(&task.id) {
task.id = new_id.clone();
}
task.dependencies = task
.dependencies
.iter()
.map(|dep| {
id_map.get(dep).cloned().unwrap_or_else(|| {
if dep.contains(':') {
dep.clone()
} else {
Task::make_id(epic_tag, dep)
}
})
})
.collect();
if let Some(ref parent) = task.parent_id {
task.parent_id = Some(
id_map
.get(parent)
.cloned()
.unwrap_or_else(|| Task::make_id(epic_tag, parent)),
);
}
task.subtasks = task
.subtasks
.iter()
.map(|sub| {
id_map
.get(sub)
.cloned()
.unwrap_or_else(|| Task::make_id(epic_tag, sub))
})
.collect();
if task.title.starts_with("[PARENT]") {
task.title = task.title.trim_start_matches("[PARENT]").trim().to_string();
task.status = TaskStatus::Expanded;
parent_fixes += 1;
}
}
let task_ids: Vec<String> = epic.tasks.iter().map(|t| t.id.clone()).collect();
let mut parent_child_links: Vec<(String, String)> = Vec::new();
for task in &epic.tasks {
let local_id = task.local_id().to_string();
if local_id.contains('.') && task.parent_id.is_none() {
if let Some(parent_local) = local_id.rsplit_once('.').map(|(p, _)| p.to_string()) {
let parent_id = Task::make_id(epic_tag, &parent_local);
if task_ids.contains(&parent_id) {
parent_child_links.push((task.id.clone(), parent_id));
}
}
}
}
for (child_id, parent_id) in parent_child_links {
if let Some(child) = epic.tasks.iter_mut().find(|t| t.id == child_id) {
child.parent_id = Some(parent_id.clone());
subtask_links += 1;
}
if let Some(parent) = epic.tasks.iter_mut().find(|t| t.id == parent_id) {
if !parent.subtasks.contains(&child_id) {
parent.subtasks.push(child_id);
}
}
}
}
for (_, epic) in all_tasks.iter_mut() {
let subtask_ids: Vec<String> = epic
.tasks
.iter()
.filter(|t| t.parent_id.is_some())
.filter_map(|t| t.parent_id.clone())
.collect();
for task in &mut epic.tasks {
if subtask_ids.contains(&task.id)
&& task.status != TaskStatus::Expanded
&& (task.status == TaskStatus::Pending || task.status == TaskStatus::InProgress)
{
task.status = TaskStatus::Expanded;
parent_fixes += 1;
}
}
}
if dry_run {
println!("{}", "Dry run - no changes made".yellow());
println!();
if changes.is_empty() && parent_fixes == 0 && subtask_links == 0 {
println!("{}", "No migrations needed. Data is up to date!".green());
return Ok(());
}
if !changes.is_empty() {
println!("{}", "ID changes:".blue().bold());
for change in &changes {
println!(" {}", change);
}
println!();
}
println!("{}", "Summary:".blue().bold());
println!(" {} ID namespacing changes", changes.len());
println!(" {} [PARENT] prefix fixes", parent_fixes);
println!(" {} subtask relationships inferred", subtask_links);
} else {
if changes.is_empty() && parent_fixes == 0 && subtask_links == 0 {
println!("{}", "No migrations needed. Data is up to date!".green());
return Ok(());
}
storage.save_tasks(&all_tasks)?;
println!("{}", "Migration complete!".green().bold());
println!();
println!(" {} task IDs namespaced", changes.len());
println!(
" {} [PARENT] prefixes converted to Expanded status",
parent_fixes
);
println!(" {} subtask relationships established", subtask_links);
println!();
println!(
"{}",
"Tip: Run 'scud list' to verify the migration.".dimmed()
);
}
Ok(())
}