bendis 0.4.0

A patch tool for Bender to work better in HERIS project
use anyhow::{bail, Context, Result};
use colored::Colorize;
use sha2::{Sha256, Digest};
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};

use crate::converter::format;
use crate::utils::config::{self, BendisConfig};

pub fn run() -> Result<()> {
    // Check for legacy structure and offer migration
    if config::check_and_migrate_if_needed()? {
        // Migration was performed or user declined
        // If migration was successful, continue with update
        // If user declined, exit
        if !config::get_bendis_dir().exists() {
            // User declined migration and no new structure exists
            bail!(
                "error: bendis_workspace directory not found\nrun `bendis init` to migrate or initialize the project"
            );
        }
    }

    println!("Updating dependencies...");

    // Load configuration
    let cfg = BendisConfig::load()?;

    let bendis_dir = config::get_bendis_dir();
    let root_dir = config::get_root_dir();

    // Check if bendis_workspace directory exists
    if !bendis_dir.exists() {
        bail!(
            "error: bendis_workspace directory not found\nrun `bendis init` first to initialize the project"
        );
    }

    // Check bendis_workspace/.gitignore if enabled in config
    let mut gitignore_warning = None;
    if cfg.gitignore_check == 1 {
        if let Err(e) = config::check_bendis_gitignore() {
            gitignore_warning = Some(e.to_string());
        }
    }

    // Step 1: Copy hw/ and target/ directories from root to bendis_workspace/
    println!("  Syncing hw/ and target/ directories to bendis_workspace/...");
    config::copy_root_dirs_to_bendis_workspace()?;

    // Step 2: Run bender update in bendis_workspace directory
    println!("  Preparing cache files...");
    run_bender_update_in_bendis(&bendis_dir, cfg.silent_mode == 1)?;

    // Step 3: Run format converter
    println!("  Converting URLs...");
    format::convert(&bendis_dir, &root_dir)?;

    // Step 4: Ensure bendis_workspace/.gitignore has required entries (hw/, target/)
    println!("  Updating bendis_workspace/.gitignore...");
    config::ensure_bendis_workspace_gitignore_entries()?;

    // Step 5: Ensure root .gitignore does not contain hw/, target/
    println!("  Updating root .gitignore...");
    config::ensure_root_gitignore_excludes_hw_target()?;

    // Step 6: Check if files changed, run bender update if needed
    println!("  Checking for changes...");
    let needs_update = check_files_changed(&root_dir)?;

    if needs_update {
        println!("  Detected changes, updating dependencies...");
        cleanup_root_files(&root_dir)?;
        run_bender_update_in_root()?;
    } else {
        println!("  No changes detected, skipping bender update");
    }

    // Step 7: Clean up based on storage_saving_mode
    println!("  Cleaning up cache...");
    cleanup_bendis_workspace_bender_dir(&bendis_dir, cfg.storage_saving_mode == 1)?;

    // Step 8: Verify
    verify_completion(&root_dir)?;

    println!("Done");

    // Display gitignore warning if there was an issue
    if let Some(warning_msg) = gitignore_warning {
        eprintln!("\n{}", "warning:".yellow().bold());
        eprintln!("  {}", warning_msg);
        eprintln!("  Some files in bendis_workspace/ may be tracked by git unexpectedly.");
        eprintln!("  Please review bendis_workspace/.gitignore and ensure it contains the required entries.");
        eprintln!("  To disable this check, set 'gitignore_check = 0' in: {}",
                  "bendis config".cyan());
    }

    Ok(())
}

fn calculate_file_hash(path: &Path) -> Result<String> {
    let content = fs::read(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;
    let hash = Sha256::digest(&content);
    Ok(format!("{:x}", hash))
}

fn check_files_changed(root_dir: &Path) -> Result<bool> {
    let bender_yml = root_dir.join("Bender.yml");
    let dot_bender_yml = root_dir.join(".bender.yml");
    let bender_lock = root_dir.join("Bender.lock");
    let bender_dir = root_dir.join(".bender");

    // Check if all required files and directories exist in root directory
    // Bender.lock is generated by bender update, so we only check existence, not hash
    if !bender_yml.exists() || !dot_bender_yml.exists() || !bender_lock.exists() || !bender_dir.exists() {
        return Ok(true);
    }

    // Calculate current hashes for configuration files only
    let current_bender_yml_hash = calculate_file_hash(&bender_yml)?;
    let current_dot_bender_yml_hash = calculate_file_hash(&dot_bender_yml)?;

    // Calculate new hashes from bendis_workspace
    let bendis_dir = config::get_bendis_dir();
    let new_bender_yml = bendis_dir.join("Bender.yml");
    let new_dot_bender_yml = root_dir.join(".bender.yml"); // This was just written by format::convert

    if !new_bender_yml.exists() {
        return Ok(true);
    }

    let new_bender_yml_hash = calculate_file_hash(&new_bender_yml)?;
    let new_dot_bender_yml_hash = calculate_file_hash(&new_dot_bender_yml)?;

    // Compare hashes for configuration files
    // Note: We don't compare Bender.lock hash because it's regenerated each time by bender
    Ok(current_bender_yml_hash != new_bender_yml_hash ||
       current_dot_bender_yml_hash != new_dot_bender_yml_hash)
}

fn run_bender_update_in_bendis(bendis_dir: &Path, silent: bool) -> Result<()> {
    let mut cmd = Command::new("bender");
    cmd.args(&["-d", "./bendis_workspace", "update"]);

    // Always capture output for error logging
    cmd.stdout(Stdio::piped());
    cmd.stderr(Stdio::piped());

    let mut child = cmd.spawn()
        .context("Failed to run bender. Is bender installed and in PATH?")?;

    // Capture stdout and stderr
    let stdout = child.stdout.take().unwrap();
    let stderr = child.stderr.take().unwrap();

    let mut stdout_reader = BufReader::new(stdout);
    let mut stderr_reader = BufReader::new(stderr);

    let mut log_buffer = Vec::new();
    let mut line = String::new();

    // Read stdout
    while stdout_reader.read_line(&mut line).unwrap_or(0) > 0 {
        if !silent {
            print!("{}", line);
        }
        log_buffer.push(line.clone());
        line.clear();
    }

    // Read stderr
    while stderr_reader.read_line(&mut line).unwrap_or(0) > 0 {
        if !silent {
            eprint!("{}", line);
        }
        log_buffer.push(line.clone());
        line.clear();
    }

    let status = child.wait()
        .context("Failed to wait for bender process")?;

    if !status.success() {
        // Always display full log on error, regardless of silent mode
        eprintln!("\n{}", "Error: bender update failed in bendis_workspace/".red());
        eprintln!("\nCommand output:");
        for log_line in &log_buffer {
            eprint!("{}", log_line);
        }

        // Check for specific fetch/version error pattern
        let has_fetch_error = log_buffer.iter().any(|line| {
            line.contains("Fetching Dependency") &&
            line.contains("cannot satisfy requirement") &&
            line.contains("may need fetch")
        });

        if has_fetch_error {
            eprintln!("\n{}", "Possible solution:".yellow());
            eprintln!("  1. Delete {} directory", "bendis_workspace/.bender/".cyan());
            eprintln!("  2. Check your internet connection");
            eprintln!("  3. Run {} again", "bendis update".cyan());
        }

        bail!("error: bender update in bendis_workspace/ failed");
    }

    // Check if Bender.lock was created
    let lock_file = bendis_dir.join("Bender.lock");
    if !lock_file.exists() {
        bail!("error: failed to generate bendis_workspace/Bender.lock");
    }

    Ok(())
}

fn cleanup_root_files(root_dir: &Path) -> Result<()> {
    // Remove old Bender.lock if exists
    let old_lock = root_dir.join("Bender.lock");
    if old_lock.exists() {
        fs::remove_file(&old_lock)
            .context("Failed to remove old Bender.lock")?;
    }

    // Remove old .bender/ directory if exists
    let old_bender_dir = root_dir.join(".bender");
    if old_bender_dir.exists() {
        fs::remove_dir_all(&old_bender_dir)
            .context("Failed to remove old .bender/ directory")?;
    }

    Ok(())
}

fn run_bender_update_in_root() -> Result<()> {
    let mut cmd = Command::new("bender");
    cmd.arg("update");

    // Always capture output for error logging
    cmd.stdout(Stdio::piped());
    cmd.stderr(Stdio::piped());

    let mut child = cmd.spawn()
        .context("Failed to run bender in root directory")?;

    // Capture stdout and stderr
    let stdout = child.stdout.take().unwrap();
    let stderr = child.stderr.take().unwrap();

    let mut stdout_reader = BufReader::new(stdout);
    let mut stderr_reader = BufReader::new(stderr);

    let mut log_buffer = Vec::new();
    let mut line = String::new();

    // Read stdout (always visible for root bender update)
    while stdout_reader.read_line(&mut line).unwrap_or(0) > 0 {
        print!("{}", line);
        log_buffer.push(line.clone());
        line.clear();
    }

    // Read stderr (always visible for root bender update)
    while stderr_reader.read_line(&mut line).unwrap_or(0) > 0 {
        eprint!("{}", line);
        log_buffer.push(line.clone());
        line.clear();
    }

    let status = child.wait()
        .context("Failed to wait for bender process")?;

    if !status.success() {
        // Display simple error message with full log
        eprintln!("\n{}", "Error: bender update failed in root directory".red());
        eprintln!("\nCommand output:");
        for log_line in &log_buffer {
            eprint!("{}", log_line);
        }

        // Check for specific fetch/version error pattern
        let has_fetch_error = log_buffer.iter().any(|line| {
            line.contains("Fetching Dependency") &&
            line.contains("cannot satisfy requirement") &&
            line.contains("may need fetch")
        });

        if has_fetch_error {
            eprintln!("\n{}", "Possible solution:".yellow());
            eprintln!("  1. Delete {} directory", ".bender/".cyan());
            eprintln!("  2. Check your internet connection");
            eprintln!("  3. Run {} again", "bendis update".cyan());
        }

        bail!("error: bender update in root directory failed");
    }

    Ok(())
}

fn cleanup_bendis_workspace_bender_dir(bendis_dir: &Path, full_cleanup: bool) -> Result<()> {
    if full_cleanup {
        // storage_saving_mode = 1: delete entire bendis_workspace/.bender/
        let bender_dir = bendis_dir.join(".bender");
        if bender_dir.exists() {
            fs::remove_dir_all(&bender_dir)
                .context("Failed to remove bendis_workspace/.bender/")?;
        }
    } else {
        // storage_saving_mode = 0: don't delete anything (keep cache)
        // Do nothing
    }

    Ok(())
}

fn verify_completion(root_dir: &Path) -> Result<()> {
    let mut missing = Vec::new();

    // Check required files
    if !root_dir.join("Bender.yml").exists() {
        missing.push("Bender.yml");
    }
    if !root_dir.join(".bender.yml").exists() {
        missing.push(".bender.yml");
    }
    if !root_dir.join("Bender.lock").exists() {
        missing.push("Bender.lock");
    }
    if !root_dir.join(".bender").exists() {
        missing.push(".bender/");
    }

    if !missing.is_empty() {
        bail!("error: verification failed, missing: {}", missing.join(", "));
    }

    Ok(())
}