nixy-rs 0.4.2

Homebrew-style wrapper for Nix using flake.nix
use std::collections::HashSet;

use crate::config::Config;
use crate::error::Result;
use crate::flake::parser::collect_local_packages;
use crate::nixy_config::{nixy_json_exists, NixyConfig};
use crate::profile::get_flake_dir;
use crate::state::{get_state_path, PackageState};

use super::info;

/// Package entry with source information
struct PackageEntry {
    name: String,
    source: PackageSource,
    /// Platform restrictions (None means all platforms)
    platforms: Option<Vec<String>>,
}

/// Source of a package
enum PackageSource {
    /// Standard nixpkgs package (legacy, no version info)
    Nixpkgs,
    /// Resolved nixpkgs package with version
    NixpkgsVersioned { version: String },
    /// Custom package from an external flake
    Custom { url: String },
    /// Local package from packages/ directory
    Local,
}

impl PackageSource {
    fn display(&self) -> String {
        match self {
            PackageSource::Nixpkgs | PackageSource::NixpkgsVersioned { .. } => {
                "nixpkgs".to_string()
            }
            PackageSource::Custom { url } => url.clone(),
            PackageSource::Local => "local".to_string(),
        }
    }
}

/// Format package name with version if available
fn format_package_name(entry: &PackageEntry) -> String {
    match &entry.source {
        PackageSource::NixpkgsVersioned { version } => {
            format!("{}@{}", entry.name, version)
        }
        _ => entry.name.clone(),
    }
}

/// Format platform restrictions for display
fn format_platforms(platforms: &Option<Vec<String>>) -> String {
    match platforms {
        None => String::new(),
        Some(p) if p.is_empty() => String::new(),
        Some(p) => {
            // Simplify platform display: if all darwin or all linux, show the alias
            let all_darwin = p.iter().all(|s| s.ends_with("-darwin"));
            let all_linux = p.iter().all(|s| s.ends_with("-linux"));
            if all_darwin && p.len() == 2 {
                " [darwin]".to_string()
            } else if all_linux && p.len() == 2 {
                " [linux]".to_string()
            } else {
                format!(" [{}]", p.join(", "))
            }
        }
    }
}

pub fn run(config: &Config) -> Result<()> {
    info("Installed packages:");

    // Collect all packages with their sources
    let mut entries: Vec<PackageEntry> = Vec::new();
    let mut seen: HashSet<String> = HashSet::new();

    // Use NixyConfig if available (new format)
    if nixy_json_exists(config) {
        let nixy_config = NixyConfig::load(config)?;

        // Add local packages first (highest priority, same as flake generation)
        if config.global_packages_dir.exists() {
            let (local_packages, local_flakes) =
                collect_local_packages(&config.global_packages_dir);
            for pkg in local_packages {
                entries.push(PackageEntry {
                    name: pkg.name.clone(),
                    source: PackageSource::Local,
                    platforms: None,
                });
                seen.insert(pkg.name);
            }
            for flake in local_flakes {
                entries.push(PackageEntry {
                    name: flake.name.clone(),
                    source: PackageSource::Local,
                    platforms: None,
                });
                seen.insert(flake.name);
            }
        }

        // Add profile entries, skipping those already covered by local packages
        if let Some(profile) = nixy_config.get_active_profile() {
            // Add legacy nixpkgs packages (no version info)
            for name in &profile.packages {
                if !seen.contains(name) {
                    entries.push(PackageEntry {
                        name: name.clone(),
                        source: PackageSource::Nixpkgs,
                        platforms: None,
                    });
                    seen.insert(name.clone());
                }
            }

            // Add resolved nixpkgs packages (with version info)
            for pkg in &profile.resolved_packages {
                if !seen.contains(&pkg.name) {
                    entries.push(PackageEntry {
                        name: pkg.name.clone(),
                        source: PackageSource::NixpkgsVersioned {
                            version: pkg.resolved_version.clone(),
                        },
                        platforms: pkg.platforms.clone(),
                    });
                    seen.insert(pkg.name.clone());
                }
            }

            // Add custom packages
            for pkg in &profile.custom_packages {
                if !seen.contains(&pkg.name) {
                    entries.push(PackageEntry {
                        name: pkg.name.clone(),
                        source: PackageSource::Custom {
                            url: pkg.input_url.clone(),
                        },
                        platforms: pkg.platforms.clone(),
                    });
                    seen.insert(pkg.name.clone());
                }
            }
        }
    } else {
        // Legacy format: Get the flake directory
        let flake_dir = get_flake_dir(config)?;
        let state_path = get_state_path(&flake_dir);

        // Load state from packages.json
        let state = PackageState::load(&state_path)?;

        // Add legacy nixpkgs packages (no version info)
        for name in &state.packages {
            entries.push(PackageEntry {
                name: name.clone(),
                source: PackageSource::Nixpkgs,
                platforms: None,
            });
            seen.insert(name.clone());
        }

        // Add resolved nixpkgs packages (with version info)
        for pkg in &state.resolved_packages {
            entries.push(PackageEntry {
                name: pkg.name.clone(),
                source: PackageSource::NixpkgsVersioned {
                    version: pkg.resolved_version.clone(),
                },
                platforms: pkg.platforms.clone(),
            });
            seen.insert(pkg.name.clone());
        }

        // Add custom packages
        for pkg in &state.custom_packages {
            entries.push(PackageEntry {
                name: pkg.name.clone(),
                source: PackageSource::Custom {
                    url: pkg.input_url.clone(),
                },
                platforms: pkg.platforms.clone(),
            });
            seen.insert(pkg.name.clone());
        }

        // Add local packages from packages/ directory
        let packages_dir = flake_dir.join("packages");
        if packages_dir.exists() {
            let (local_packages, local_flakes) = collect_local_packages(&packages_dir);
            for pkg in local_packages {
                if !seen.contains(&pkg.name) {
                    entries.push(PackageEntry {
                        name: pkg.name.clone(),
                        source: PackageSource::Local,
                        platforms: None,
                    });
                    seen.insert(pkg.name);
                }
            }
            for flake in local_flakes {
                if !seen.contains(&flake.name) {
                    entries.push(PackageEntry {
                        name: flake.name.clone(),
                        source: PackageSource::Local,
                        platforms: None,
                    });
                    seen.insert(flake.name);
                }
            }
        }
    }

    // Sort by name
    entries.sort_by(|a, b| a.name.cmp(&b.name));

    if entries.is_empty() {
        println!("  (none)");
    } else {
        // Calculate column width for alignment (using formatted name with version)
        let max_name_len = entries
            .iter()
            .map(|e| format_package_name(e).len())
            .max()
            .unwrap_or(0);

        for entry in entries {
            let formatted_name = format_package_name(&entry);
            let platform_str = format_platforms(&entry.platforms);
            println!(
                "  {:<width$}  ({}){}",
                formatted_name,
                entry.source.display(),
                platform_str,
                width = max_name_len
            );
        }
    }

    Ok(())
}