nolo 0.1.1

A CLI tool for discovering and analyzing `TODO` comments across codebases.
Documentation
use crate::analytics::TodoAnalytics;
use tabled::{
    Table, Tabled,
    settings::{Alignment, Style},
};

// Distribution visualization constants
const DISTRIBUTION_SCALE: f64 = 5.0; // Each bar represents 5%
const DISTRIBUTION_WIDTH: usize = 20;

#[derive(Tabled)]
struct SummaryRow {
    #[tabled(rename = "Metric")]
    metric: String,
    #[tabled(rename = "Value")]
    value: String,
}

#[derive(Tabled)]
struct KeywordRow {
    #[tabled(rename = "Keyword")]
    keyword: String,
    #[tabled(rename = "Count")]
    count: usize,
    #[tabled(rename = "Percentage")]
    percentage: String,
    #[tabled(rename = "Distribution")]
    distribution: String,
}

#[derive(Tabled)]
struct LanguageRow {
    #[tabled(rename = "Language")]
    language: String,
    #[tabled(rename = "TODOs")]
    todo_count: usize,
    #[tabled(rename = "Files")]
    file_count: usize,
    #[tabled(rename = "Extensions")]
    extensions: String,
}

#[derive(Tabled)]
struct FileRow {
    #[tabled(rename = "File")]
    file: String,
    #[tabled(rename = "TODO Count")]
    todo_count: usize,
}

pub fn print_table_report(analytics: &TodoAnalytics) {
    print_header(analytics);
    print_summary_table(analytics);
    print_keyword_table(analytics);
    print_language_table(analytics);
    print_file_table(analytics);
    print_footer();
}

fn print_header(analytics: &TodoAnalytics) {
    println!(
        "TODO Report for {}",
        get_relative_path(&analytics.scan_path, &analytics.scan_path)
    );
    println!(
        "Generated: {}",
        chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
    );
    println!();
}

fn print_summary_table(analytics: &TodoAnalytics) {
    let summary_data = vec![
        SummaryRow {
            metric: "Total TODO Comments".to_string(),
            value: analytics.total_todos.to_string(),
        },
        SummaryRow {
            metric: "Files with TODOs".to_string(),
            value: analytics.files_with_todos.to_string(),
        },
        SummaryRow {
            metric: "Total Files Scanned".to_string(),
            value: analytics.total_files_scanned.to_string(),
        },
        SummaryRow {
            metric: "TODO Density".to_string(),
            value: format!("{:.1}%", analytics.todo_density()),
        },
    ];

    println!("{}", create_styled_table(summary_data));
    println!();
}

fn print_keyword_table(analytics: &TodoAnalytics) {
    if analytics.keyword_counts.is_empty() {
        return;
    }

    let mut keywords: Vec<_> = analytics.keyword_counts.iter().collect();
    keywords.sort_by(|a, b| b.1.cmp(a.1));

    let keyword_data: Vec<KeywordRow> = keywords
        .iter()
        .map(|(keyword, count)| {
            let percentage = if analytics.total_todos > 0 {
                (**count as f64 / analytics.total_todos as f64) * 100.0
            } else {
                0.0
            };
            let bar_count = (percentage / DISTRIBUTION_SCALE) as usize;
            let distribution_bars = "".repeat(bar_count);

            KeywordRow {
                keyword: keyword.to_string(),
                count: **count,
                percentage: format!("{:.1}%", percentage),
                distribution: format!("{:<width$}", distribution_bars, width = DISTRIBUTION_WIDTH),
            }
        })
        .collect();

    println!("{}", create_styled_table(keyword_data));
    println!();
}

fn print_language_table(analytics: &TodoAnalytics) {
    if analytics.language_stats.is_empty() {
        return;
    }

    let mut languages: Vec<_> = analytics.language_stats.iter().collect();
    languages.sort_by(|a, b| b.1.todo_count.cmp(&a.1.todo_count));

    let language_data: Vec<LanguageRow> = languages
        .iter()
        .map(|(language, stats)| LanguageRow {
            language: language.to_string(),
            todo_count: stats.todo_count,
            file_count: stats.file_count,
            extensions: stats.extensions.join(", "),
        })
        .collect();

    println!("{}", create_styled_table(language_data));
    println!();
}

fn print_file_table(analytics: &TodoAnalytics) {
    if analytics.top_files.is_empty() {
        return;
    }

    let file_data: Vec<FileRow> = analytics
        .top_files
        .iter()
        .map(|file_stat| FileRow {
            file: get_relative_path(&file_stat.path, &analytics.scan_path),
            todo_count: file_stat.todo_count,
        })
        .collect();

    println!("{}", create_styled_table(file_data));

    if analytics.files_with_todos > analytics.top_files.len() {
        let omitted = analytics.files_with_todos - analytics.top_files.len();
        println!("({} omitted, use --full-report to show all)", omitted);
    }
}

fn print_footer() {
    println!();
    println!("Use 'nolo scan' to see detailed TODO list.");
}

fn create_styled_table<T: Tabled>(data: Vec<T>) -> String {
    Table::new(data)
        .with(Style::modern())
        .with(Alignment::left())
        .to_string()
}

pub fn get_relative_path(file_path: &std::path::Path, scan_path: &std::path::Path) -> String {
    if scan_path.is_file() && file_path == scan_path {
        return scan_path
            .file_name()
            .map(|name| name.to_string_lossy().to_string())
            .unwrap_or_else(|| file_path.display().to_string());
    }

    if let Ok(relative) = file_path.strip_prefix(scan_path) {
        let relative_str = relative.display().to_string();
        if relative_str.is_empty() {
            ".".to_string()
        } else {
            relative_str
        }
    } else {
        file_path.display().to_string()
    }
}