sysmap 0.2.0

Project Mapping CLI Tool
use std::env;
use std::path::PathBuf;

use anyhow::Result;
use colored::Colorize;

use crate::config::{find_sysmap_root, map_path};
use crate::map::SystemMap;
use crate::scanner::{estimate_tokens, format_tokens};

/// Execute the deps command
pub fn execute(file: PathBuf, reverse: bool, json: bool) -> Result<()> {
    let cwd = env::current_dir()?;

    let root = find_sysmap_root(&cwd)
        .ok_or_else(|| anyhow::anyhow!("No sysmap found. Run 'sysmap init' first."))?;

    let map = SystemMap::load(&map_path(&root))?;

    // Normalize the file path to be relative to project root
    let file_path = normalize_path(&file, &cwd, &map.root)?;

    if reverse {
        show_reverse_deps(&map, &file_path, json)
    } else {
        show_deps(&map, &file_path, json)
    }
}

/// Normalize a file path to be relative to the project root
fn normalize_path(file: &PathBuf, cwd: &PathBuf, project_root: &PathBuf) -> Result<PathBuf> {
    // If it's already relative, try to resolve it
    let absolute = if file.is_absolute() {
        file.clone()
    } else {
        cwd.join(file)
    };

    // Make it relative to project root
    if let Ok(rel) = absolute.strip_prefix(project_root) {
        Ok(rel.to_path_buf())
    } else if let Ok(rel) = file.strip_prefix(project_root) {
        Ok(rel.to_path_buf())
    } else {
        // Assume it's already relative to project root
        Ok(file.clone())
    }
}

/// Show dependencies of a file
fn show_deps(map: &SystemMap, file_path: &PathBuf, json: bool) -> Result<()> {
    let internal = map.dependencies.internal.get(file_path);
    let external = map.dependencies.external.get(file_path);

    // Get file info from tree
    let file_info = find_file_info(&map, file_path);

    if json {
        print_json_deps(file_path, internal, external, file_info)
    } else {
        print_human_deps(file_path, internal, external, file_info)
    }
}

/// Show what files depend on this file (reverse lookup)
fn show_reverse_deps(map: &SystemMap, file_path: &PathBuf, json: bool) -> Result<()> {
    // Find all files that have this file in their internal dependencies
    let mut dependents: Vec<&PathBuf> = Vec::new();

    for (source, deps) in &map.dependencies.internal {
        if deps.contains(file_path) {
            dependents.push(source);
        }
    }

    dependents.sort();

    // Get file info
    let file_info = find_file_info(&map, file_path);

    if json {
        print_json_reverse_deps(file_path, &dependents, file_info)
    } else {
        print_human_reverse_deps(file_path, &dependents, file_info)
    }
}

/// File info extracted from the tree
struct FileInfo {
    lines: Option<usize>,
    chars: Option<usize>,
    language: Option<String>,
}

/// Find file info in the tree
fn find_file_info(map: &SystemMap, file_path: &PathBuf) -> Option<FileInfo> {
    find_file_info_recursive(&map.tree, file_path)
}

fn find_file_info_recursive(node: &crate::map::FileNode, target: &PathBuf) -> Option<FileInfo> {
    match node {
        crate::map::FileNode::File { path, lines, chars, language, .. } => {
            if path == target {
                Some(FileInfo {
                    lines: *lines,
                    chars: *chars,
                    language: language.clone(),
                })
            } else {
                None
            }
        }
        crate::map::FileNode::Directory { children, .. } => {
            for child in children {
                if let Some(info) = find_file_info_recursive(child, target) {
                    return Some(info);
                }
            }
            None
        }
        crate::map::FileNode::Collapsed { .. } => None,
    }
}

/// Print human-readable deps output
fn print_human_deps(
    file_path: &PathBuf,
    internal: Option<&Vec<PathBuf>>,
    external: Option<&Vec<String>>,
    file_info: Option<FileInfo>,
) -> Result<()> {
    // Header with file info
    print!("{}", file_path.display().to_string().bold());
    if let Some(info) = &file_info {
        let mut parts = Vec::new();
        if let Some(lines) = info.lines {
            parts.push(format!("{} lines", lines));
        }
        if let Some(chars) = info.chars {
            let tokens = estimate_tokens(chars);
            parts.push(format!("{} chars ({})", chars, format_tokens(tokens)));
        }
        if !parts.is_empty() {
            print!(" {}", format!("({})", parts.join(", ")).dimmed());
        }
    }
    println!();

    // Internal dependencies
    let has_internal = internal.map(|v| !v.is_empty()).unwrap_or(false);
    let has_external = external.map(|v| !v.is_empty()).unwrap_or(false);

    if !has_internal && !has_external {
        println!("  {}", "No dependencies found".dimmed());
        return Ok(());
    }

    if has_internal {
        println!("  {}:", "internal".cyan());
        for dep in internal.unwrap() {
            println!("    ├── {}", dep.display());
        }
    }

    if has_external {
        println!("  {}:", "external".yellow());
        let ext = external.unwrap();
        for (i, dep) in ext.iter().enumerate() {
            let prefix = if i == ext.len() - 1 { "└──" } else { "├──" };
            println!("    {} {}", prefix, dep);
        }
    }

    Ok(())
}

/// Print human-readable reverse deps output
fn print_human_reverse_deps(
    file_path: &PathBuf,
    dependents: &[&PathBuf],
    file_info: Option<FileInfo>,
) -> Result<()> {
    // Header with file info
    print!("{}", file_path.display().to_string().bold());
    if let Some(info) = &file_info {
        let mut parts = Vec::new();
        if let Some(lines) = info.lines {
            parts.push(format!("{} lines", lines));
        }
        if !parts.is_empty() {
            print!(" {}", format!("({})", parts.join(", ")).dimmed());
        }
    }
    println!();

    if dependents.is_empty() {
        println!("  {}", "No files depend on this".dimmed());
        return Ok(());
    }

    println!("  {}:", "imported by".green());
    for (i, dep) in dependents.iter().enumerate() {
        let prefix = if i == dependents.len() - 1 { "└──" } else { "├──" };
        println!("    {} {}", prefix, dep.display());
    }

    Ok(())
}

/// Print JSON deps output
fn print_json_deps(
    file_path: &PathBuf,
    internal: Option<&Vec<PathBuf>>,
    external: Option<&Vec<String>>,
    file_info: Option<FileInfo>,
) -> Result<()> {
    let output = serde_json::json!({
        "file": file_path,
        "lines": file_info.as_ref().and_then(|i| i.lines),
        "chars": file_info.as_ref().and_then(|i| i.chars),
        "language": file_info.as_ref().and_then(|i| i.language.clone()),
        "dependencies": {
            "internal": internal.unwrap_or(&Vec::new()),
            "external": external.unwrap_or(&Vec::new())
        }
    });

    println!("{}", serde_json::to_string_pretty(&output)?);
    Ok(())
}

/// Print JSON reverse deps output
fn print_json_reverse_deps(
    file_path: &PathBuf,
    dependents: &[&PathBuf],
    file_info: Option<FileInfo>,
) -> Result<()> {
    let output = serde_json::json!({
        "file": file_path,
        "lines": file_info.as_ref().and_then(|i| i.lines),
        "chars": file_info.as_ref().and_then(|i| i.chars),
        "language": file_info.as_ref().and_then(|i| i.language.clone()),
        "imported_by": dependents
    });

    println!("{}", serde_json::to_string_pretty(&output)?);
    Ok(())
}