bendis 0.1.0

A patch tool for Bender to work better in HERIS project
use anyhow::{Context, Result};
use colored::Colorize;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;

#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
enum Source {
    Git(String),
    Path(String),
}

#[derive(Debug, Deserialize, Serialize, Clone)]
struct Package {
    revision: Option<String>,
    version: Option<String>,
    source: Source,
    dependencies: Vec<String>,
}

#[derive(Debug, Deserialize, Serialize)]
struct LockFile {
    packages: HashMap<String, Package>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
enum DependencySpec {
    Simple(String),
    Detailed(HashMap<String, serde_yaml::Value>),
}

#[derive(Debug, Deserialize, Serialize)]
struct BenderYml {
    dependencies: Option<HashMap<String, DependencySpec>>,
}

#[derive(Debug, Deserialize, Serialize)]
struct DotBenderYml {
    overrides: Option<HashMap<String, HashMap<String, serde_yaml::Value>>>,
}

/// Convert GitHub URL to IHEP internal Git URL
fn convert_url(git_url: &str) -> String {
    if git_url.contains("github.com/pulp-platform") {
        // Extract repository name
        if let Some(repo_name) = git_url.split('/').last() {
            return format!("git@code.ihep.ac.cn:heris/heris-platform/{}", repo_name);
        }
    }
    git_url.to_string()
}

/// Extract dependencies from lock file
fn extract_dependencies_from_lock(lock_data: &LockFile) -> HashMap<String, HashMap<String, serde_yaml::Value>> {
    let mut dependencies = HashMap::new();

    for (pkg_name, pkg_info) in &lock_data.packages {
        match &pkg_info.source {
            Source::Git(git_url) => {
                let converted_url = convert_url(git_url);
                let mut dep_info = HashMap::new();
                dep_info.insert("git".to_string(), serde_yaml::Value::String(converted_url));

                // Add version or revision
                if let Some(version) = &pkg_info.version {
                    dep_info.insert("version".to_string(), serde_yaml::to_value(version).unwrap());
                } else if let Some(revision) = &pkg_info.revision {
                    dep_info.insert("rev".to_string(), serde_yaml::Value::String(revision.clone()));
                }

                dependencies.insert(pkg_name.clone(), dep_info);
            }
            Source::Path(path) => {
                let mut dep_info = HashMap::new();
                dep_info.insert("path".to_string(), serde_yaml::Value::String(path.clone()));
                dependencies.insert(pkg_name.clone(), dep_info);
            }
        }
    }

    dependencies
}

/// Extract dependency names from Bender.yml
fn extract_dependencies_from_yml(yml_data: &BenderYml) -> HashSet<String> {
    if let Some(deps) = &yml_data.dependencies {
        deps.keys().cloned().collect()
    } else {
        HashSet::new()
    }
}

/// Extract existing overrides from .bender.yml
fn extract_overrides_from_bender_yml(bender_yml_data: &DotBenderYml) -> HashMap<String, HashMap<String, serde_yaml::Value>> {
    if let Some(overrides) = &bender_yml_data.overrides {
        overrides.clone()
    } else {
        HashMap::new()
    }
}

/// Compare two version strings
fn compare_versions(v1: &Option<String>, v2: &Option<String>) -> i8 {
    match (v1, v2) {
        (None, None) => 0,
        (None, Some(_)) => -1,
        (Some(_), None) => 1,
        (Some(v1_str), Some(v2_str)) => {
            let v1_parts: Vec<&str> = v1_str.split('.').collect();
            let v2_parts: Vec<&str> = v2_str.split('.').collect();

            let max_len = v1_parts.len().max(v2_parts.len());

            for i in 0..max_len {
                let p1 = v1_parts.get(i).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
                let p2 = v2_parts.get(i).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);

                if p1 > p2 {
                    return 1;
                } else if p1 < p2 {
                    return -1;
                }
            }
            0
        }
    }
}

/// Find missing dependencies that need to be added to overrides
fn find_missing_dependencies(
    lock_deps: &HashMap<String, HashMap<String, serde_yaml::Value>>,
    yml_deps: &HashSet<String>,
    existing_overrides: &HashMap<String, HashMap<String, serde_yaml::Value>>,
) -> HashMap<String, HashMap<String, serde_yaml::Value>> {
    let mut missing_deps = HashMap::new();

    for (dep_name, dep_info) in lock_deps {
        // Skip if already in Bender.yml
        if yml_deps.contains(dep_name) {
            continue;
        }

        // Check if already in overrides
        if let Some(existing_info) = existing_overrides.get(dep_name) {
            // Both have git URLs, check versions
            if dep_info.contains_key("git") && existing_info.contains_key("git") {
                let new_version = dep_info.get("version").and_then(|v| {
                    if let serde_yaml::Value::String(s) = v {
                        Some(s.clone())
                    } else {
                        v.as_f64().map(|f| f.to_string())
                    }
                });

                let existing_version = existing_info.get("version").and_then(|v| {
                    if let serde_yaml::Value::String(s) = v {
                        Some(s.clone())
                    } else {
                        v.as_f64().map(|f| f.to_string())
                    }
                });

                // If new version is newer, update it
                if compare_versions(&new_version, &existing_version) > 0 {
                    missing_deps.insert(dep_name.clone(), dep_info.clone());
                }
            } else if dep_info.contains_key("git") && existing_info.contains_key("path") {
                // Prefer git over path
                missing_deps.insert(dep_name.clone(), dep_info.clone());
            }
        } else {
            // Not in overrides, add it
            missing_deps.insert(dep_name.clone(), dep_info.clone());
        }
    }

    missing_deps
}

/// Format a single dependency line in inline YAML style
fn format_dependency_line(
    name: &str,
    dep_info: &HashMap<String, serde_yaml::Value>,
    max_name_len: usize,
    max_url_len: usize,
) -> String {
    let name_padding = " ".repeat(max_name_len.saturating_sub(name.len()));

    if let Some(serde_yaml::Value::String(path)) = dep_info.get("path") {
        let url_part = format!(r#"{{ path: "{}" "#, path);
        let url_padding = " ".repeat(max_url_len.saturating_sub(url_part.len()));
        format!("  {}:{} {}{}}}", name, name_padding, url_part, url_padding)
    } else if let Some(serde_yaml::Value::String(git_url)) = dep_info.get("git") {
        let git_part = format!(r#"{{ git: "{}"#, git_url);

        if let Some(version) = dep_info.get("version") {
            let version_str = match version {
                serde_yaml::Value::String(s) => s.clone(),
                serde_yaml::Value::Number(n) => n.to_string(),
                _ => format!("{:?}", version),
            };
            let git_part_with_comma = format!("{},", git_part);
            let url_padding = " ".repeat(max_url_len.saturating_sub(git_part_with_comma.len()));
            format!("  {}:{} {}{} version: {} }}", name, name_padding, git_part_with_comma, url_padding, version_str)
        } else if let Some(serde_yaml::Value::String(rev)) = dep_info.get("rev") {
            let git_part_with_comma = format!("{},", git_part);
            let url_padding = " ".repeat(max_url_len.saturating_sub(git_part_with_comma.len()));
            format!(r#"  {}:{} {}{} rev: "{}" }}"#, name, name_padding, git_part_with_comma, url_padding, rev)
        } else {
            let url_padding = " ".repeat(max_url_len.saturating_sub(git_part.len()));
            format!("  {}:{} {}{} }}", name, name_padding, git_part, url_padding)
        }
    } else {
        format!("  {}:{} {{}}", name, name_padding)
    }
}

/// Generate formatted override lines
fn generate_overrides_lines(all_overrides: &HashMap<String, HashMap<String, serde_yaml::Value>>) -> Vec<String> {
    if all_overrides.is_empty() {
        return Vec::new();
    }

    // Calculate maximum name length
    let max_name_len = all_overrides.keys().map(|k| k.len()).max().unwrap_or(0);

    // Calculate maximum URL part length
    let mut max_url_len = 0;
    for dep_info in all_overrides.values() {
        let url_part_len = if let Some(serde_yaml::Value::String(path)) = dep_info.get("path") {
            format!(r#"{{ path: "{}" "#, path).len()
        } else if let Some(serde_yaml::Value::String(git_url)) = dep_info.get("git") {
            if dep_info.contains_key("version") || dep_info.contains_key("rev") {
                format!(r#"{{ git: "{}","#, git_url).len()
            } else {
                format!(r#"{{ git: "{}"#, git_url).len()
            }
        } else {
            0
        };
        max_url_len = max_url_len.max(url_part_len);
    }

    let mut lines = Vec::new();
    for (name, dep_info) in all_overrides {
        lines.push(format_dependency_line(name, dep_info, max_name_len, max_url_len));
    }

    lines
}

/// Update .bender.yml with new overrides
fn update_bender_yml_overrides(
    bender_yml_text: &str,
    existing_overrides: &HashMap<String, HashMap<String, serde_yaml::Value>>,
    new_overrides: &HashMap<String, HashMap<String, serde_yaml::Value>>,
) -> String {
    // Merge overrides
    let mut all_overrides = existing_overrides.clone();
    all_overrides.extend(new_overrides.clone());

    // Generate formatted override lines
    let override_lines = generate_overrides_lines(&all_overrides);

    if override_lines.is_empty() {
        return bender_yml_text.to_string();
    }

    // Build new overrides section
    let new_overrides_section = format!("overrides:\n{}", override_lines.join("\n"));

    // Replace overrides section
    let pattern = Regex::new(r"overrides:.*?(?=\n[a-z_]+:|$)").unwrap();
    let updated_text = pattern.replace(bender_yml_text, new_overrides_section.as_str());

    updated_text.to_string()
}

/// Main conversion function
pub fn convert(
    bendis_dir: &Path,
    root_dir: &Path,
) -> Result<()> {
    println!("{}", "Running format converter...".bold().cyan());

    // Read input files
    let lock_path = bendis_dir.join("Bender.lock");
    let yml_path = bendis_dir.join("Bender.yml");
    let bender_yml_path = bendis_dir.join(".bender.yml");

    println!("  {} Reading {}", "".blue(), lock_path.display());
    let lock_content = fs::read_to_string(&lock_path)
        .context("Failed to read .bendis/Bender.lock")?;
    let lock_data: LockFile = serde_yaml::from_str(&lock_content)
        .context("Failed to parse Bender.lock")?;

    println!("  {} Reading {}", "".blue(), yml_path.display());
    let yml_content = fs::read_to_string(&yml_path)
        .context("Failed to read .bendis/Bender.yml")?;
    let yml_data: BenderYml = serde_yaml::from_str(&yml_content)
        .unwrap_or(BenderYml { dependencies: None });

    println!("  {} Reading {}", "".blue(), bender_yml_path.display());
    let bender_yml_text = fs::read_to_string(&bender_yml_path)
        .context("Failed to read .bendis/.bender.yml")?;
    let bender_yml_data: DotBenderYml = serde_yaml::from_str(&bender_yml_text)
        .unwrap_or(DotBenderYml { overrides: None });

    // Extract dependencies
    println!("  {} Extracting dependencies from lock file", "".blue());
    let lock_deps = extract_dependencies_from_lock(&lock_data);

    println!("  {} Extracting dependencies from Bender.yml", "".blue());
    let yml_deps = extract_dependencies_from_yml(&yml_data);

    println!("  {} Extracting existing overrides", "".blue());
    let existing_overrides = extract_overrides_from_bender_yml(&bender_yml_data);

    // Find missing dependencies
    println!("  {} Finding missing/updated dependencies", "".blue());
    let missing_deps = find_missing_dependencies(&lock_deps, &yml_deps, &existing_overrides);

    if !missing_deps.is_empty() {
        println!("  {} Found {} dependencies to add/update:", "".green(), missing_deps.len());
        for dep_name in missing_deps.keys() {
            println!("      - {}", dep_name.yellow());
        }
    } else {
        println!("  {} No new dependencies to add", "".blue());
    }

    // Copy Bender.yml to root
    println!("  {} Copying Bender.yml to root", "".blue());
    fs::copy(&yml_path, root_dir.join("Bender.yml"))
        .context("Failed to copy Bender.yml to root")?;

    // Update .bender.yml
    println!("  {} Generating new .bender.yml", "".blue());
    let updated_bender_yml = update_bender_yml_overrides(&bender_yml_text, &existing_overrides, &missing_deps);

    // Write to root
    let output_path = root_dir.join(".bender.yml");
    fs::write(&output_path, updated_bender_yml)
        .context("Failed to write .bender.yml to root")?;

    println!("  {} Wrote {}", "".green(), output_path.display());
    println!(
        "\n  {} Total dependencies in lock: {}",
        "📊".to_string(),
        lock_deps.len()
    );
    println!(
        "  {} Dependencies in Bender.yml: {}",
        "📊".to_string(),
        yml_deps.len()
    );
    println!(
        "  {} Added/updated in overrides: {}",
        "📊".to_string(),
        missing_deps.len()
    );

    Ok(())
}