// Copyright (c) 2026 Joydev GmbH (joydev.com)
// SPDX-License-Identifier: MIT

use anyhow::Result;
use clap::Args;

use joy_core::items;
use joy_core::milestones;
use joy_core::model::item::{Item, ItemType, Priority, Status};
use joy_core::store;
use joy_core::vcs::Vcs;

use crate::color;

#[derive(Args)]
pub struct LsArgs {
    /// Filter by ancestor item ID (shows the item and all descendants)
    #[arg(long)]
    parent: Option<String>,

    /// Filter by type: epic, story, task, bug, rework, decision, idea
    #[arg(short = 'T', long = "type")]
    item_type: Option<String>,

    /// Filter by status: new, open, in-progress, review, closed, deferred
    #[arg(short, long)]
    status: Option<String>,

    /// Filter by priority: low, medium, high, critical, extreme
    #[arg(short, long)]
    priority: Option<String>,

    /// Show only items assigned to me (git config user.email)
    #[arg(short = 'M', long)]
    mine: bool,

    /// Filter by milestone ID (includes items inheriting from parent)
    #[arg(short, long)]
    milestone: Option<String>,

    /// Filter by tag
    #[arg(long)]
    tag: Option<String>,

    /// Filter by version tag
    #[arg(short = 'v', long)]
    version: Option<String>,

    /// Show only blocked items
    #[arg(short, long)]
    blocked: bool,

    /// Show all items (including closed and deferred)
    #[arg(short, long)]
    all: bool,

    /// Show hierarchical tree view
    #[arg(long)]
    tree: bool,

    /// Extra columns to show (comma-separated: milestone, assignee, parent)
    #[arg(short, long, value_delimiter = ',')]
    columns: Vec<String>,

    /// Group tree view by: parent (default), milestone
    #[arg(short, long, default_value = "parent")]
    group: String,

    /// Reverse sort order (newest first instead of oldest first)
    #[arg(short, long)]
    reverse: bool,

    /// Compact output: emoji-only or abbreviations
    #[arg(short = 'S', long)]
    pub short: bool,
}

impl LsArgs {
    pub fn roadmap(all: bool) -> Self {
        Self {
            parent: None,
            item_type: None,
            status: None,
            priority: None,
            mine: false,
            milestone: None,
            tag: None,
            version: None,
            blocked: false,
            all,
            tree: true,
            columns: Vec::new(),
            group: "milestone".to_string(),
            reverse: false,
            short: false,
        }
    }
}

/// Resolve the effective milestone for an item: its own, or inherited from ancestors.
pub fn effective_milestone<'a>(item: &'a Item, all_items: &'a [Item]) -> Option<&'a str> {
    if let Some(ref ms) = item.milestone {
        return Some(ms.as_str());
    }
    let mut visited = std::collections::HashSet::new();
    let mut current_parent = item.parent.as_deref();
    while let Some(pid) = current_parent {
        if !visited.insert(pid) {
            break; // cycle detected
        }
        if let Some(parent) = all_items.iter().find(|i| i.id == pid) {
            if let Some(ref ms) = parent.milestone {
                return Some(ms.as_str());
            }
            current_parent = parent.parent.as_deref();
        } else {
            break;
        }
    }
    None
}

/// Check if an item is a descendant of the given ancestor.
fn is_descendant(item: &Item, ancestor_id: &str, all_items: &[Item]) -> bool {
    let mut visited = std::collections::HashSet::new();
    let mut current = item.parent.as_deref();
    while let Some(pid) = current {
        if !visited.insert(pid) {
            break; // cycle detected
        }
        if pid == ancestor_id {
            return true;
        }
        current = all_items
            .iter()
            .find(|i| i.id == pid)
            .and_then(|i| i.parent.as_deref());
    }
    false
}

/// Which extra columns to display in table mode.
struct ExtraColumns {
    milestone: bool,
    assignee: bool,
    parent: bool,
}

impl ExtraColumns {
    fn from_args(columns: &[String]) -> Self {
        Self {
            milestone: columns.iter().any(|s| s == "milestone" || s == "ms"),
            assignee: columns.iter().any(|s| s == "assignee"),
            parent: columns.iter().any(|s| s == "parent"),
        }
    }
}

pub fn run(args: LsArgs) -> Result<()> {
    let cwd = std::env::current_dir()?;
    let root = store::find_project_root(&cwd).ok_or(joy_core::error::JoyError::NotInitialized)?;

    let all_items = items::load_items(&root)?;

    if all_items.is_empty() {
        println!("No items. Run `joy add` to create one.");
        return Ok(());
    }

    let my_email: Option<String> = if args.mine {
        Some(
            joy_core::vcs::default_vcs()
                .user_email()
                .map_err(|e| anyhow::anyhow!("{e}"))?,
        )
    } else {
        None
    };

    let type_filter: Option<ItemType> = args
        .item_type
        .as_deref()
        .map(|t| t.parse().map_err(|e: String| anyhow::anyhow!("{}", e)))
        .transpose()?;

    let status_filter: Option<Status> = args
        .status
        .as_deref()
        .map(|s| s.parse().map_err(|e: String| anyhow::anyhow!("{}", e)))
        .transpose()?;

    let priority_filter: Option<Priority> = args
        .priority
        .as_deref()
        .map(|p| p.parse().map_err(|e: String| anyhow::anyhow!("{}", e)))
        .transpose()?;

    let mut filtered: Vec<&Item> = all_items
        .iter()
        .filter(|item| {
            // By default, exclude closed and deferred
            if !args.all
                && args.status.is_none()
                && matches!(item.status, Status::Closed | Status::Deferred)
            {
                return false;
            }

            if let Some(ref parent_id) = args.parent {
                if item.id != *parent_id && !is_descendant(item, parent_id, &all_items) {
                    return false;
                }
            }

            if let Some(ref t) = type_filter {
                if &item.item_type != t {
                    return false;
                }
            }

            if let Some(ref s) = status_filter {
                if &item.status != s {
                    return false;
                }
            }

            if let Some(ref p) = priority_filter {
                if &item.priority != p {
                    return false;
                }
            }

            if let Some(ref ms) = args.milestone {
                if effective_milestone(item, &all_items) != Some(ms.as_str()) {
                    return false;
                }
            }

            if let Some(ref tag) = args.tag {
                if !item.tags.iter().any(|t| t == tag) {
                    return false;
                }
            }

            if let Some(ref version) = args.version {
                if item.version.as_deref() != Some(version.as_str()) {
                    return false;
                }
            }

            if let Some(ref email) = my_email {
                if !item.assignees.iter().any(|a| a.member == *email) {
                    return false;
                }
            }

            if args.blocked && !item.is_blocked_by(&all_items) {
                return false;
            }

            true
        })
        .collect();

    if filtered.is_empty() {
        println!("No matching items.");
        return Ok(());
    }

    if args.reverse {
        filtered.reverse();
    }

    let extras = ExtraColumns::from_args(&args.columns);

    if args.tree {
        match args.group.as_str() {
            "milestone" | "ms" => {
                let ms_list = milestones::load_milestones(&root)?;
                let project = store::read_yaml::<joy_core::model::Project>(
                    &store::joy_dir(&root).join(store::PROJECT_FILE),
                )?;
                print_tree_by_milestone(&filtered, &ms_list, &all_items, &project);
            }
            "parent" => print_tree_by_parent(&filtered),
            other => anyhow::bail!("unknown group: {other} (use: parent, milestone)"),
        }
    } else {
        print_table(&filtered, &all_items, &extras);
    }

    Ok(())
}

/// Detect terminal width: crossterm -> COLUMNS env var -> 80.
fn terminal_width() -> usize {
    #[cfg(feature = "tui")]
    {
        if let Ok((cols, _)) = crossterm::terminal::size() {
            return cols as usize;
        }
    }
    std::env::var("COLUMNS")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(80)
}

/// Truncate a string to `max_len` display columns, appending "..." if truncated.
fn truncate_title(s: &str, max_len: usize) -> String {
    if display_width(s) <= max_len {
        return s.to_string();
    }
    if max_len <= 3 {
        return ".".repeat(max_len);
    }
    let target = max_len - 3;
    let mut width = 0;
    let mut end = 0;
    for c in s.chars() {
        let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
        if width + w > target {
            break;
        }
        width += w;
        end += c.len_utf8();
    }
    format!("{}...", &s[..end])
}

fn print_table(items: &[&Item], all_items: &[Item], extras: &ExtraColumns) {
    let term_width = terminal_width();

    // Pass 1: compute dynamic column widths from data
    let col_raw = |header: &str, f: &dyn Fn(&Item) -> String| -> usize {
        items
            .iter()
            .map(|i| display_width(&f(i)))
            .max()
            .unwrap_or(0)
            .max(display_width(header))
    };

    let w_id = col_raw("ID", &|i| i.id.clone());
    let w_type = col_raw("TYPE", &|i| color::item_type_display(&i.item_type).0);
    let w_status = col_raw("STATUS", &|i| color::status_display(&i.status).0);
    let w_prio = col_raw("PRIO", &|i| color::priority_display(&i.priority).0);
    let w_eff = col_raw("EFF", &|i| {
        i.effort
            .map(|e| e.to_string())
            .unwrap_or_else(|| " ".into())
    });

    let w_parent = if extras.parent {
        col_raw("PARENT", &|i| {
            i.parent.as_deref().unwrap_or("-").to_string()
        })
    } else {
        0
    };
    let w_ms = if extras.milestone {
        col_raw("MS", &|i| match i.milestone.as_deref() {
            Some(ms) => ms.to_string(),
            None => match effective_milestone(i, all_items) {
                Some(ms) => format!("{ms}*"),
                None => "-".to_string(),
            },
        })
    } else {
        0
    };
    let w_assignee = if extras.assignee {
        col_raw("ASSIGNEE", &|i| {
            if i.assignees.is_empty() {
                "-".to_string()
            } else {
                i.assignees
                    .iter()
                    .map(|a| a.member.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            }
        })
    } else {
        0
    };

    // Total fixed width (each column + 1 space separator)
    let fixed_width = w_id
        + 1
        + w_type
        + 1
        + w_status
        + 1
        + w_prio
        + 1
        + w_eff
        + 1
        + if extras.parent { w_parent + 1 } else { 0 }
        + if extras.milestone { w_ms + 1 } else { 0 }
        + if extras.assignee { w_assignee + 1 } else { 0 };

    let min_title_width = 20;
    let title_width = if term_width > fixed_width {
        (term_width - fixed_width).max(min_title_width)
    } else {
        min_title_width
    };

    // Pass 2: print header
    let sep_len = term_width.min(fixed_width + title_width);
    println!("{}", color::label(&"-".repeat(sep_len)));

    let mut header = format!(
        "{} {} {} {} {}",
        pad_colored(&color::label("ID"), "ID", w_id),
        pad_colored(&color::label("TYPE"), "TYPE", w_type),
        pad_colored(&color::label("STATUS"), "STATUS", w_status),
        pad_colored(&color::label("PRIO"), "PRIO", w_prio),
        pad_colored(&color::label("EFF"), "EFF", w_eff),
    );
    if extras.parent {
        header.push_str(&format!(
            " {}",
            pad_colored(&color::label("PARENT"), "PARENT", w_parent)
        ));
    }
    if extras.milestone {
        header.push_str(&format!(
            " {}",
            pad_colored(&color::label("MS"), "MS", w_ms)
        ));
    }
    if extras.assignee {
        header.push_str(&format!(
            " {}",
            pad_colored(&color::label("ASSIGNEE"), "ASSIGNEE", w_assignee)
        ));
    }
    header.push_str(&format!(" {}", color::label("TITLE")));
    println!("{header}");

    println!("{}", color::label(&"-".repeat(sep_len)));

    // Pass 2: print rows
    for item in items {
        let blocked_str = if item.is_blocked_by(all_items) {
            " [blocked]".to_string()
        } else {
            String::new()
        };

        let raw_title = format!("{}{}", item.title, blocked_str);
        let display_title = truncate_title(&raw_title, title_width);

        let colored_title = if !blocked_str.is_empty() && display_title.ends_with("[blocked]") {
            let prefix_len = display_title.len() - "[blocked]".len();
            let prefix = &display_title[..prefix_len];
            format!("{}{}", prefix, color::blocked("[blocked]"))
        } else {
            display_title
        };

        let id_str = color::id(&item.id);
        let (type_raw, type_str) = color::item_type_display(&item.item_type);
        let (status_raw, status_str) = color::status_display(&item.status);
        let (priority_raw, priority_str) = color::priority_display(&item.priority);

        let eff_str = color::effort_indicator(item.effort);
        let eff_raw = item
            .effort
            .map(|e| e.to_string())
            .unwrap_or_else(|| " ".to_string());

        let mut line = format!(
            "{} {} {} {} {}",
            pad_colored(&id_str, &item.id, w_id),
            pad_colored(&type_str, &type_raw, w_type),
            pad_colored(&status_str, &status_raw, w_status),
            pad_colored(&priority_str, &priority_raw, w_prio),
            pad_colored(&eff_str, &eff_raw, w_eff),
        );

        if extras.parent {
            let parent_val = item.parent.as_deref().unwrap_or("-");
            line.push_str(&format!(
                " {}",
                pad_colored(&color::id(parent_val), parent_val, w_parent)
            ));
        }
        if extras.milestone {
            let (ms_val, inherited) = match item.milestone.as_deref() {
                Some(ms) => (ms, false),
                None => match effective_milestone(item, all_items) {
                    Some(ms) => (ms, true),
                    None => ("-", false),
                },
            };
            let display = if inherited {
                format!("{ms_val}*")
            } else {
                ms_val.to_string()
            };
            line.push_str(&format!(
                " {}",
                pad_colored(&color::id(&display), &display, w_ms)
            ));
        }
        if extras.assignee {
            let assignee_val = if item.assignees.is_empty() {
                "-".to_string()
            } else {
                item.assignees
                    .iter()
                    .map(|a| a.member.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            };
            let assignee_val = assignee_val.as_str();
            line.push_str(&format!(
                " {}",
                pad_colored(assignee_val, assignee_val, w_assignee)
            ));
        }

        line.push_str(&format!(" {}", colored_title));
        println!("{line}");
    }

    let w = terminal_width();
    println!("{}", color::label(&"-".repeat(w)));
    println!("{}", color::label(&color::plural(items.len(), "item")));
}

fn display_width(s: &str) -> usize {
    unicode_width::UnicodeWidthStr::width(s)
}

fn pad_colored(colored: &str, raw: &str, width: usize) -> String {
    let padding = width.saturating_sub(display_width(raw));
    format!("{}{}", colored, " ".repeat(padding))
}

// -- Tree by parent hierarchy (recursive) --

fn print_tree_by_parent(items: &[&Item]) {
    let w = terminal_width();
    println!("{}", color::header("Tree"));

    let item_ids: std::collections::HashSet<&str> = items.iter().map(|i| i.id.as_str()).collect();

    // Root items: no parent, or parent not in the filtered set
    let roots: Vec<&&Item> = items
        .iter()
        .filter(|i| match i.parent.as_deref() {
            None => true,
            Some(pid) => !item_ids.contains(pid),
        })
        .collect();

    for (i, root) in roots.iter().enumerate() {
        let is_last = i == roots.len() - 1;
        print_tree_node(root, items, "", is_last);
    }

    println!("{}", color::label(&"-".repeat(w)));
    println!("{}", color::label(&color::plural(items.len(), "item")));
}

/// Compute the available title width for a tree row, given the prefix and item metadata.
fn tree_title_width(prefix: &str, connector: &str, item: &Item) -> usize {
    let term_width = terminal_width();
    let (type_raw, _) = color::item_type_display(&item.item_type);
    let (status_raw, _) = color::status_display(&item.status);
    let (prio_raw, _) = color::priority_display(&item.priority);

    // Fixed parts: prefix + connector + "ID " + " [type] [status] [prio]"
    let chrome_width = display_width(prefix)
        + display_width(connector)
        + display_width(&item.id)
        + 1 // space after ID
        + 3 + display_width(&type_raw)   // " [type]"
        + 3 + display_width(&status_raw) // " [status]"
        + 3 + display_width(&prio_raw); // " [prio]"

    term_width.saturating_sub(chrome_width)
}

fn print_tree_node(item: &Item, all_items: &[&Item], prefix: &str, is_last: bool) {
    let connector = if prefix.is_empty() {
        String::new() // Root level: no connector
    } else if is_last {
        "└── ".to_string()
    } else {
        "├── ".to_string()
    };

    let child_prefix = if prefix.is_empty() {
        "  ".to_string()
    } else if is_last {
        format!("{prefix}    ")
    } else {
        format!("{prefix}│   ")
    };

    let tree_chrome = color::label(&format!("{prefix}{connector}"));

    // Find children in the filtered set
    let children: Vec<&&Item> = all_items
        .iter()
        .filter(|i| i.parent.as_deref() == Some(&item.id))
        .collect();

    let title_width = tree_title_width(prefix, &connector, item);
    let title = truncate_title(&item.title, title_width);

    let (type_raw, type_colored) = color::item_type_display(&item.item_type);
    let (prio_raw, prio_colored) = color::priority_display(&item.priority);

    // Right-align attributes: calculate how much space the left side uses,
    // then pad to push attributes to the right terminal edge.
    let left_raw = format!("{}{}{} {}", prefix, connector, item.id, title);
    let left_width = display_width(&left_raw);
    let attr_raw = format!(
        " [{}] [{}] [{}]",
        type_raw,
        color::status_display(&item.status).0,
        prio_raw
    );
    let attr_width = display_width(&attr_raw);
    let term_width = terminal_width();
    let padding = term_width.saturating_sub(left_width + attr_width);
    let spacer = " ".repeat(padding);

    if !item.is_active() {
        let (status_raw, _) = color::status_display(&item.status);
        println!(
            "{}{}",
            tree_chrome,
            color::inactive(&format!(
                "{} {}{} [{}] [{}] [{}]",
                item.id, title, spacer, type_raw, status_raw, prio_raw
            ))
        );
    } else {
        let (_, status_colored) = color::status_display(&item.status);
        println!(
            "{}{} {}{} [{}] [{}] [{}]",
            tree_chrome,
            color::id(&item.id),
            title,
            spacer,
            type_colored,
            status_colored,
            prio_colored
        );
    }

    for (ci, child) in children.iter().enumerate() {
        let child_is_last = ci == children.len() - 1;
        print_tree_node(child, all_items, &child_prefix, child_is_last);
    }
}

// -- Tree by milestone (with parent sub-grouping) --

/// Count all items (including closed) belonging to a milestone via effective_milestone.
fn milestone_counts(ms_id: &str, all_items: &[Item]) -> (usize, usize) {
    let linked: Vec<_> = all_items
        .iter()
        .filter(|i| effective_milestone(i, all_items) == Some(ms_id))
        .collect();
    let closed = linked.iter().filter(|i| !i.is_active()).count();
    (closed, linked.len())
}

fn print_tree_by_milestone(
    items: &[&Item],
    ms_list: &[joy_core::model::Milestone],
    all_items: &[Item],
    project: &joy_core::model::Project,
) {
    use std::collections::{BTreeMap, HashSet};

    // Header (matches board style)
    let total = all_items.len();
    let closed = all_items.iter().filter(|i| !i.is_active()).count();
    let ms_count = ms_list.len();
    let header_info = format!(
        "{} · {} milestones · {}/{} items closed",
        project.name, ms_count, closed, total
    );
    let w = terminal_width();
    let sep = color::label(&"-".repeat(w));
    println!("{}", sep);
    println!("{}", color::label(&header_info));
    println!("{}", sep);

    // Collect all milestone IDs using effective milestone (own or inherited from parent)
    let mut groups: BTreeMap<String, Vec<&&Item>> = BTreeMap::new();
    let mut no_milestone: Vec<&&Item> = Vec::new();

    for item in items {
        match effective_milestone(item, all_items) {
            Some(ms_id) => groups.entry(ms_id.to_string()).or_default().push(item),
            None => no_milestone.push(item),
        }
    }

    let known_ids: HashSet<&str> = ms_list.iter().map(|ms| ms.id.as_str()).collect();
    let mut first = true;

    // Print known milestones first (in their defined order)
    for ms in ms_list {
        let children = groups.remove(&ms.id);
        let (closed, total) = milestone_counts(&ms.id, all_items);
        if !first {
            println!();
        }
        first = false;
        let empty = vec![];
        print_milestone_group(
            &ms.id,
            Some(&ms.title),
            ms.date.as_ref(),
            closed,
            total,
            children.as_deref().unwrap_or(&empty),
        );
    }

    // Print unknown milestone IDs (referenced but no .yaml file)
    for (ms_id, children) in &groups {
        if !known_ids.contains(ms_id.as_str()) {
            if !first {
                println!();
            }
            first = false;
            let (closed, total) = milestone_counts(ms_id, all_items);
            print_milestone_group(ms_id, None, None, closed, total, children);
        }
    }

    // Items without milestone
    if !no_milestone.is_empty() {
        if !first {
            println!();
        }
        let no_ms_total: Vec<_> = all_items
            .iter()
            .filter(|i| effective_milestone(i, all_items).is_none())
            .collect();
        let no_ms_closed = no_ms_total.iter().filter(|i| !i.is_active()).count();
        println!(
            "{} [{}/{}]",
            color::label("(no milestone)"),
            no_ms_closed,
            no_ms_total.len()
        );
        print_parent_grouped_children(&no_milestone);
    }

    // Footer with statistics (matches board style)
    let new = all_items.iter().filter(|i| i.status == Status::New).count();
    let open = all_items
        .iter()
        .filter(|i| i.status == Status::Open)
        .count();
    let wip = all_items
        .iter()
        .filter(|i| i.status == Status::InProgress)
        .count();
    let review = all_items
        .iter()
        .filter(|i| i.status == Status::Review)
        .count();
    let deferred = all_items
        .iter()
        .filter(|i| i.status == Status::Deferred)
        .count();
    let blocked_count = all_items
        .iter()
        .filter(|i| i.is_blocked_by(all_items))
        .count();

    let mut stats = Vec::new();
    if new > 0 {
        stats.push(format!("{new} new"));
    }
    if open > 0 {
        stats.push(format!("{open} open"));
    }
    if wip > 0 {
        stats.push(format!("{wip} in-progress"));
    }
    if review > 0 {
        stats.push(format!("{review} review"));
    }
    if closed > 0 {
        stats.push(format!("{closed} closed"));
    }
    if deferred > 0 {
        stats.push(format!("{deferred} deferred"));
    }
    if blocked_count > 0 {
        stats.push(format!("{blocked_count} blocked"));
    }

    println!("{}", sep);
    println!("{}", color::label(&stats.join(" · ")));
}

fn print_milestone_group(
    id: &str,
    title: Option<&str>,
    date: Option<&chrono::NaiveDate>,
    closed: usize,
    total: usize,
    children: &[&&Item],
) {
    let date_str = date.map(|d| format!(" ({})", d)).unwrap_or_default();
    let title_str = title.unwrap_or("(undefined)");

    println!(
        "{} {}{} [{}/{}]",
        color::id(id),
        color::label(title_str),
        color::label(&date_str),
        closed,
        total
    );
    print_parent_grouped_children(children);
}

/// Print children grouped by parent hierarchy within a milestone group.
fn print_parent_grouped_children(items: &[&&Item]) {
    let item_ids: std::collections::HashSet<&str> = items.iter().map(|i| i.id.as_str()).collect();

    // Root items within this group: no parent, or parent not in this group
    let roots: Vec<&&&Item> = items
        .iter()
        .filter(|i| match i.parent.as_deref() {
            None => true,
            Some(pid) => !item_ids.contains(pid),
        })
        .collect();

    for (i, root) in roots.iter().enumerate() {
        let is_last = i == roots.len() - 1;
        print_ms_tree_node(root, items, "  ", is_last);
    }
}

fn print_ms_tree_node(item: &Item, group: &[&&Item], prefix: &str, is_last: bool) {
    let connector = if is_last { "└── " } else { "├── " };
    let child_prefix = if is_last {
        format!("{prefix}    ")
    } else {
        format!("{prefix}│   ")
    };

    let tree_chrome = color::label(&format!("{prefix}{connector}"));

    let children: Vec<&&&Item> = group
        .iter()
        .filter(|i| i.parent.as_deref() == Some(&item.id))
        .collect();

    let title_width = tree_title_width(prefix, connector, item);
    let title = truncate_title(&item.title, title_width);

    let (type_raw, type_colored) = color::item_type_display(&item.item_type);
    let (prio_raw, prio_colored) = color::priority_display(&item.priority);

    // Right-align attributes
    let left_raw = format!("{}{}{} {}", prefix, connector, item.id, title);
    let left_width = display_width(&left_raw);
    let attr_raw = format!(
        " [{}] [{}] [{}]",
        type_raw,
        color::status_display(&item.status).0,
        prio_raw
    );
    let attr_width = display_width(&attr_raw);
    let term_width = terminal_width();
    let padding = term_width.saturating_sub(left_width + attr_width);
    let spacer = " ".repeat(padding);

    if !item.is_active() {
        let (status_raw, _) = color::status_display(&item.status);
        println!(
            "{}{}",
            tree_chrome,
            color::inactive(&format!(
                "{} {}{} [{}] [{}] [{}]",
                item.id, title, spacer, type_raw, status_raw, prio_raw
            ))
        );
    } else {
        let (_, status_colored) = color::status_display(&item.status);
        println!(
            "{}{} {}{} [{}] [{}] [{}]",
            tree_chrome,
            color::id(&item.id),
            title,
            spacer,
            type_colored,
            status_colored,
            prio_colored
        );
    }

    for (ci, child) in children.iter().enumerate() {
        let child_is_last = ci == children.len() - 1;
        print_ms_tree_node(child, group, &child_prefix, child_is_last);
    }
}
