todoscope 0.1.0

A simple CLI todo list manager
Documentation
use crate::input::{AddTodoInput, SearchTodoInput, UpdateTodoInput};
use crate::sortby::SortBy;
use crate::storage::{load_todos_from_file, save_todos_to_file};
use crate::todo::Todo;
use chrono::Utc;
use colored::*;
use uuid::Uuid;

pub fn add_todo_cli(file_path: &str, todo_input: AddTodoInput) {
    let mut todos = load_todos_from_file(file_path);

    let todo = Todo::new(todo_input);

    if todos.contains_key(&todo.id) {
        println!(
            "{}",
            "❌ A todo with this ID already exists. Try again."
                .red()
                .bold()
        );
        return;
    }

    todos.insert(todo.id, todo);
    save_todos_to_file(&todos, file_path);
    println!("{}", "✅ Todo added successfully".green().bold());
}

pub fn list_todos_cli(file_path: &str, sort_by: &SortBy) {
    let todos = load_todos_from_file(file_path);

    if todos.is_empty() {
        println!("{}", "❌ No todos found.".red().bold());
        return;
    }

    let mut todo_list: Vec<&Todo> = todos.values().collect();

    match sort_by {
        SortBy::Priority => todo_list.sort_by_key(|t| t.priority),
        SortBy::Status => todo_list.sort_by_key(|t| t.status),
        SortBy::Created => todo_list.sort_by_key(|t| t.created_at),
        SortBy::DueDate => todo_list.sort_by_key(|t| t.due_date),
        SortBy::Overdue => {
            let now = Utc::now();
            todo_list.sort_by_key(|t| match t.due_date {
                Some(due) if due < now => 0,
                Some(_) => 1,
                None => 2,
            });
        }
    }

    println!(
        "{}",
        format!("--- Todos (sorted by {sort_by}) ---")
            .bold()
            .blue()
            .underline()
    );
    for todo in &todo_list {
        let priority_str = todo.priority.to_string();
        let priority_color = match priority_str.as_str() {
            "High" => priority_str.red().bold(),
            "Medium" => priority_str.yellow(),
            "Low" => priority_str.green(),
            _ => priority_str.normal(),
        };

        let status_str = todo.status.to_string();
        let status_color = match status_str.as_str() {
            "Pending" => status_str.red().bold(),
            "In Progress" => status_str.yellow(),
            "Done" => status_str.green(),
            _ => status_str.normal(),
        };

        println!("{:<10} {}", "ID:".bold(), todo.id.to_string().cyan());
        println!("{:<10} {}", "Title:".bold(), todo.title.bold());
        println!("{:<10} {}", "Priority:".bold(), priority_color);
        println!("{:<10} {}", "Status:".bold(), status_color);
        println!(
            "{:<10} {}",
            "Description:".bold(),
            todo.description.as_deref().unwrap_or("None")
        );
        println!("{:<10} {}", "Created:".bold(), todo.created_at);

        if let Some(due) = todo.due_date {
            let mut overdue_str = due.to_string();
            overdue_str.push_str("⚠️ Overdue!");
            if due < Utc::now() {
                println!("{:<10} {}", "Due Date:".bold(), overdue_str.red());
            } else {
                println!("{:<10} {}", "Due Date:".bold(), due.to_string().yellow());
            }
        }

        if let Some(tags) = &todo.tags {
            println!("{:<10} {}", "Tags:".bold(), tags.join(", ").cyan());
        }

        if let Some(pid) = todo.parent_id {
            println!("{:<10} {}", "Parent ID:".bold(), pid.to_string().blue());
        }

        if let Some(subs) = &todo.subtasks {
            let subs_str: Vec<String> = subs.iter().map(|id| id.to_string()).collect();
            println!(
                "{:<10} {}",
                "Subtasks:".bold(),
                subs_str.join(", ").purple()
            );
        }

        if let Some(rec) = &todo.recurrence {
            println!("{:<10} {}", "Recurrence:".bold(), rec.to_string().yellow());
        }
        println!();
    }
}

pub fn search_todo_cli(file_path: &str, todo_input: SearchTodoInput) {
    let todos = load_todos_from_file(file_path);
    let mut results: Vec<&Todo> = todos.values().collect();

    if let Some(id_str) = todo_input.id {
        if let Ok(uuid) = Uuid::parse_str(&id_str) {
            results.retain(|t| t.id == uuid);
        } else {
            println!("{}", format!("⚠️ Invalid UUID format: {id_str}").red());
            return;
        }
    }

    if let Some(title_query) = todo_input.title {
        let query_lower = title_query.to_lowercase();
        results.retain(|t| t.title.to_lowercase().contains(&query_lower));
    }

    if let Some(p) = todo_input.priority {
        //let p = super::parse_priority(&priority_str);
        results.retain(|t| t.priority == p);
    }

    if let Some(s) = todo_input.status {
        //let s = super::parse_status(&status_str);
        results.retain(|t| t.status == s);
    }

    if let Some(due) = todo_input.due_date {
        results.retain(|t| t.due_date == Some(due));
    }

    if let Some(rec) = todo_input.recurrence {
        results.retain(|t| t.recurrence.as_ref() == Some(&rec));
    }

    if let Some(tag_lit) = todo_input.tags {
        results.retain(|t| {
            if let Some(todo_tags) = &t.tags {
                tag_lit.iter().all(|tag| todo_tags.contains(tag))
            } else {
                false
            }
        });
    }

    if let Some(pid) = todo_input.parent_id {
        results.retain(|t| t.parent_id == Some(pid));
    }

    if results.is_empty() {
        println!("{}", "⚠️ No todos found with the given filters.".yellow());
    } else {
        println!(
            "{}",
            format!("Found {} todo(s):", results.len()).bold().blue()
        );
        for todo in results {
            let priority_str = todo.priority.to_string();
            let priority_color = match priority_str.as_str() {
                "High" => priority_str.red().bold(),
                "Medium" => priority_str.yellow(),
                "Low" => priority_str.green(),
                _ => priority_str.normal(),
            };

            let status_str = todo.status.to_string();
            let status_color = match status_str.as_str() {
                "Pending" => status_str.red().bold(),
                "In Progress" => status_str.yellow(),
                "Done" => status_str.green(),
                _ => status_str.normal(),
            };
            println!("{:<10} {}", "ID:".bold(), todo.id.to_string().cyan());
            println!("{:<10} {}", "Title:".bold(), todo.title.bold());
            println!("{:<10} {}", "Priority:".bold(), priority_color);
            println!("{:<10} {}", "Status:".bold(), status_color);
            println!(
                "{:<10} {}",
                "Description:".bold(),
                todo.description.as_deref().unwrap_or("None")
            );
            println!("{:<10} {}", "Created:".bold(), todo.created_at);
            if let Some(due) = todo.due_date {
                let mut overdue_str = due.to_string();
                overdue_str.push_str(" ⚠️ Overdue!");
                if due < Utc::now() {
                    println!("{:<10} {}", "Due Date:".bold(), overdue_str.red());
                } else {
                    println!("{:<10} {}", "Due Date:".bold(), due.to_string().yellow());
                }
            }

            if let Some(tags) = &todo.tags {
                println!("{:<10} {}", "Tags:".bold(), tags.join(", ").cyan());
            }

            if let Some(pid) = todo.parent_id {
                println!("{:<10} {}", "Parent ID:".bold(), pid.to_string().blue());
            }

            if let Some(subs) = &todo.subtasks {
                let subs_str: Vec<String> = subs.iter().map(|id| id.to_string()).collect();
                println!(
                    "{:<10} {}",
                    "Subtasks:".bold(),
                    subs_str.join(", ").purple()
                );
            }

            if let Some(rec) = &todo.recurrence {
                println!("{:<10} {}", "Recurrence:".bold(), rec.to_string().yellow());
            }
            println!();
        }
    }
}

pub fn update_todo_cli(file_path: &str, todo_input: UpdateTodoInput) -> bool {
    let mut todos = load_todos_from_file(file_path);

    if let Some(todo) = todos.get_mut(&todo_input.id) {
        if let Some(title) = todo_input.new_title {
            todo.title = title
        }
        if let Some(desc) = todo_input.new_description {
            todo.description = Some(desc)
        }
        if let Some(p) = todo_input.new_priority {
            todo.priority = p;
        }
        if let Some(s) = todo_input.new_status {
            todo.status = s;
        }
        if let Some(d) = todo_input.new_due_date {
            todo.due_date = Some(d);
        }
        if let Some(tags) = todo_input.new_tags {
            todo.tags = Some(tags);
        }
        if let Some(rec) = todo_input.new_recurrence {
            todo.recurrence = Some(rec);
        }
        if let Some(pid) = todo_input.new_parent_id {
            todo.parent_id = Some(pid);
        }
        if let Some(subs) = todo_input.new_subtasks {
            todo.subtasks = Some(subs);
        }
        save_todos_to_file(&todos, file_path);
        true
    } else {
        false
    }
}

pub fn delete_todo_cli(file_path: &str, id: Uuid) -> bool {
    let mut todos = load_todos_from_file(file_path);
    if todos.remove(&id).is_some() {
        save_todos_to_file(&todos, file_path);
        true
    } else {
        false
    }
}