update_project.rs 1005 lignes · 31370 octets
use std::fs;
use std::path::{Path, PathBuf};

use console::style;
use dialoguer::{Confirm, Input};

use crate::version;

/// Fichiers et dossiers du framework qui sont remplaces lors de la mise a jour.
/// `rustwarden-core` est toujours synchronise. Les autres crates ne sont
/// synchronisees que si elles sont presentes dans le projet consommateur
/// (detection en debut de `run()`).
const FRAMEWORK_CRATES: &[&str] = &[
    "rustwarden-core",
    "rustwarden-ui",
    "rustwarden-fediverse",
    "rustwarden-fediverse-ui",
    "rustwarden-fediverse-guard",
];

/// Fichiers du projet consommateur qu'on ne touche JAMAIS.
const PROTECTED_FILES: &[&str] = &[
    "Cargo.toml",
    "src/main.rs",
    ".env",
    ".env.example",
    "README.md",
    "DEVELOPER.md",
];

/// Lance la commande update-project.
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
    println!(
        "\n{}",
        style("  Rustwarden - Mise a jour du framework  ")
            .bold()
            .on_cyan()
            .black(),
    );
    println!();

    let rustwarden_root = find_rustwarden_root()?;
    let project_dir = ask_project_dir()?;

    validate_consumer_project(&project_dir)?;

    let current_version = version::read_version(&project_dir);
    let new_version = version::compute_version(&rustwarden_root)?;

    print_version_info(&current_version, &new_version);

    let has_ui = project_dir.join("crates/rustwarden-ui").exists();
    let has_fediverse = project_dir.join("crates/rustwarden-fediverse").exists();
    let has_fediverse_ui = project_dir.join("crates/rustwarden-fediverse-ui").exists();
    let has_fediverse_guard = project_dir
        .join("crates/rustwarden-fediverse-guard")
        .exists();
    let custom_templates = detect_custom_templates(&project_dir, &rustwarden_root, has_ui);

    // Si la fédération (ou une de ses crates) manque, proposer de l'ajouter.
    let mut newly_added: Vec<&'static str> = Vec::new();
    let (has_fediverse, has_fediverse_ui, has_fediverse_guard) = prompt_fediverse_addition(
        has_fediverse,
        has_fediverse_ui,
        has_fediverse_guard,
        &mut newly_added,
    )?;

    print_update_plan(
        &project_dir,
        has_ui,
        has_fediverse,
        has_fediverse_ui,
        has_fediverse_guard,
        &newly_added,
        &custom_templates,
    );

    if !Confirm::new()
        .with_prompt(format!("{}", style("Proceder a la mise a jour ?").cyan()))
        .default(false)
        .interact()?
    {
        println!("  {} Operation annulee.", style("✗").red());
        return Ok(());
    }

    // --- Backup ---
    let backup_dir = create_backup(&project_dir)?;

    // --- Mise a jour des crates ---
    let stats = update_crates(
        &rustwarden_root,
        &project_dir,
        has_ui,
        has_fediverse,
        has_fediverse_ui,
        has_fediverse_guard,
        &custom_templates,
    )?;

    // --- Resync des docs framework-owned (SERVICES_REFERENCE, MIGRATION_GUIDE, …) ---
    refresh_docs(&rustwarden_root, &project_dir, has_fediverse)?;

    // --- Reecrire le fichier de version ---
    version::write_version(&project_dir, &new_version)?;

    // --- Resume ---
    print_summary(
        &project_dir,
        &backup_dir,
        &custom_templates,
        &newly_added,
        &stats,
    );

    Ok(())
}

/// Quand le projet n'inclut pas (encore) la fédération ActivityPub,
/// propose à l'utilisateur de l'ajouter pendant l'update. Retourne le
/// nouveau triplet `(has_fediverse, has_fediverse_ui, has_fediverse_guard)`
/// et pousse dans `newly_added` les noms de crates effectivement ajoutées.
fn prompt_fediverse_addition(
    mut has_fediverse: bool,
    mut has_fediverse_ui: bool,
    mut has_fediverse_guard: bool,
    newly_added: &mut Vec<&'static str>,
) -> Result<(bool, bool, bool), Box<dyn std::error::Error>> {
    // Cas 1 : la fédération est absente — proposer le bundle complet.
    if !has_fediverse {
        println!("\n{}", style("── Fédération ActivityPub ──").cyan().bold());
        println!(
            "  Ce projet n'inclut pas la fédération (rustwarden-fediverse).\n  \
             Périmètre, features Cargo et câblage : docs/fediverse-consumer-guide.md"
        );
        if !Confirm::new()
            .with_prompt(format!(
                "{}",
                style("Ajouter le support fédération à ce projet ?").cyan()
            ))
            .default(false)
            .interact()?
        {
            return Ok((has_fediverse, has_fediverse_ui, has_fediverse_guard));
        }
        has_fediverse = true;
        newly_added.push("rustwarden-fediverse");
    }

    // Cas 2 / extension : -ui absent.
    if !has_fediverse_ui
        && Confirm::new()
            .with_prompt(format!(
                "{}",
                style("Inclure aussi l'UI HTML (rustwarden-fediverse-ui) ?").cyan()
            ))
            .default(false)
            .interact()?
    {
        has_fediverse_ui = true;
        newly_added.push("rustwarden-fediverse-ui");
    }

    // Cas 3 / extension : -guard absent.
    if !has_fediverse_guard
        && Confirm::new()
            .with_prompt(format!(
                "{}",
                style("Inclure aussi le guard anti-abus (rustwarden-fediverse-guard) ?").cyan()
            ))
            .default(false)
            .interact()?
    {
        has_fediverse_guard = true;
        newly_added.push("rustwarden-fediverse-guard");
    }

    Ok((has_fediverse, has_fediverse_ui, has_fediverse_guard))
}

// ---------------------------------------------------------------------------
// Detection du projet consommateur
// ---------------------------------------------------------------------------

/// Demande le chemin du projet consommateur.
fn ask_project_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let dir: String = Input::new()
        .with_prompt(format!(
            "{}",
            style("Chemin du projet consommateur a mettre a jour").cyan()
        ))
        .default(".".into())
        .interact_text()?;

    let path = PathBuf::from(&dir);
    let path = if path.is_absolute() {
        path
    } else {
        std::env::current_dir()?.join(path)
    };

    Ok(path.canonicalize().unwrap_or(path))
}

/// Verifie que le dossier est bien un projet consommateur Rustwarden.
fn validate_consumer_project(project_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
    if !project_dir.join("crates/rustwarden-core").exists() {
        return Err(format!(
            "Le dossier {} ne semble pas etre un projet Rustwarden.\n\
             (crates/rustwarden-core/ introuvable)",
            project_dir.display()
        )
        .into());
    }

    if !project_dir.join("Cargo.toml").exists() {
        return Err(format!(
            "Pas de Cargo.toml dans {}. Est-ce bien la racine du projet ?",
            project_dir.display()
        )
        .into());
    }

    Ok(())
}

// ---------------------------------------------------------------------------
// Detection des templates personnalises
// ---------------------------------------------------------------------------

/// Compare les templates du projet avec ceux du framework pour detecter les
/// fichiers modifies par le consommateur.
fn detect_custom_templates(
    project_dir: &Path,
    rustwarden_root: &Path,
    has_ui: bool,
) -> Vec<String> {
    let mut custom = Vec::new();

    if !has_ui {
        return custom;
    }

    let project_templates = project_dir.join("crates/rustwarden-ui/templates");
    let framework_templates = rustwarden_root.join("crates/rustwarden-ui/templates");

    if !project_templates.exists() || !framework_templates.exists() {
        return custom;
    }

    compare_dirs_recursive(
        &project_templates,
        &framework_templates,
        &project_templates,
        &mut custom,
    );
    custom
}

/// Compare recursivement deux dossiers et enregistre les fichiers differents.
fn compare_dirs_recursive(
    project_dir: &Path,
    framework_dir: &Path,
    base: &Path,
    custom: &mut Vec<String>,
) {
    let entries = match fs::read_dir(project_dir) {
        Ok(e) => e,
        Err(_) => return,
    };

    for entry in entries.flatten() {
        let path = entry.path();
        let relative = path.strip_prefix(base).unwrap_or(&path);
        let framework_path = framework_dir.join(relative);

        if path.is_dir() {
            compare_dirs_recursive(&path, &framework_dir.join(entry.file_name()), base, custom);
        } else if path.extension().and_then(|e| e.to_str()) == Some("html") {
            if !framework_path.exists() {
                // Fichier qui n'existe pas dans le framework = fichier custom
                custom.push(relative.to_string_lossy().to_string());
            } else {
                // Comparer le contenu
                let Ok(project_content) = fs::read(&path) else {
                    continue;
                };
                let Ok(framework_content) = fs::read(&framework_path) else {
                    continue;
                };
                if project_content != framework_content {
                    custom.push(relative.to_string_lossy().to_string());
                }
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Affichage
// ---------------------------------------------------------------------------

/// Affiche les informations de version.
fn print_version_info(current: &Option<String>, new: &str) {
    println!("\n{}", style("── Versions ──").cyan().bold());

    match current {
        Some(v) => println!("  {}  {}", style("Actuelle :").dim(), style(v).white()),
        None => println!(
            "  {}  {}",
            style("Actuelle :").dim(),
            style("inconnue (pas de .rustwarden-version)").yellow()
        ),
    }
    println!("  {}  {}", style("Nouvelle :").dim(), style(new).green());
}

/// Affiche le plan de mise a jour.
fn print_update_plan(
    project_dir: &Path,
    has_ui: bool,
    has_fediverse: bool,
    has_fediverse_ui: bool,
    has_fediverse_guard: bool,
    newly_added: &[&'static str],
    custom_templates: &[String],
) {
    println!("\n{}", style("── Plan de mise a jour ──").cyan().bold());

    println!("  {} {}", style("Projet :").dim(), project_dir.display());

    println!("\n  {} :", style("Sera mis a jour").green().bold());
    println!("    - crates/rustwarden-core/");
    if has_ui {
        println!("    - crates/rustwarden-ui/");
    }
    let added = |name: &str| newly_added.contains(&name);
    if has_fediverse {
        let tag = if added("rustwarden-fediverse") {
            style(" (NOUVEAU)").green().bold().to_string()
        } else {
            String::new()
        };
        println!("    - crates/rustwarden-fediverse/{tag}");
    }
    if has_fediverse_ui {
        let tag = if added("rustwarden-fediverse-ui") {
            style(" (NOUVEAU)").green().bold().to_string()
        } else {
            String::new()
        };
        println!("    - crates/rustwarden-fediverse-ui/{tag}");
    }
    if has_fediverse_guard {
        let tag = if added("rustwarden-fediverse-guard") {
            style(" (NOUVEAU)").green().bold().to_string()
        } else {
            String::new()
        };
        println!("    - crates/rustwarden-fediverse-guard/{tag}");
    }

    println!(
        "\n  {} :",
        style("Preserve (jamais touche)").yellow().bold()
    );
    for f in PROTECTED_FILES {
        if project_dir.join(f).exists() {
            println!("    - {f}");
        }
    }

    if !custom_templates.is_empty() {
        println!(
            "\n  {} {} template(s) personnalise(s) detecte(s) :",
            style("⚠").yellow().bold(),
            custom_templates.len()
        );
        for t in custom_templates {
            println!("    - templates/{t}");
        }
        println!(
            "  {}",
            style("  Ces fichiers seront preserves (non ecrases).").green()
        );
    }

    println!(
        "\n  {}",
        style("Seuls les fichiers modifies dans le framework seront mis a jour.").dim()
    );
    println!(
        "  {}",
        style("Un backup complet des crates sera cree avant la mise a jour.").dim()
    );
}

/// Statistiques de synchronisation.
#[derive(Default)]
struct SyncStats {
    updated: Vec<String>,
    added: Vec<String>,
    skipped_custom: Vec<String>,
    unchanged: u32,
    removed: Vec<String>,
}

/// Affiche le resume final.
fn print_summary(
    project_dir: &Path,
    backup_dir: &Path,
    custom_templates: &[String],
    newly_added: &[&'static str],
    stats: &SyncStats,
) {
    let divider = style("─".repeat(55)).dim();
    println!("\n{divider}");
    println!("  {}", style("Mise a jour terminee !").green().bold());
    println!("{divider}\n");

    println!("  {} {}", style("Backup :").bold(), backup_dir.display());

    if !stats.added.is_empty() {
        println!(
            "\n  {} {} fichier(s) ajoute(s) :",
            style("+").green().bold(),
            stats.added.len()
        );
        for f in &stats.added {
            println!("    {}", style(f).green());
        }
    }

    if !stats.updated.is_empty() {
        println!(
            "\n  {} {} fichier(s) mis a jour :",
            style("~").cyan().bold(),
            stats.updated.len()
        );
        for f in &stats.updated {
            println!("    {}", style(f).cyan());
        }
    }

    if !stats.removed.is_empty() {
        println!(
            "\n  {} {} fichier(s) supprime(s) (n'existent plus dans le framework) :",
            style("-").red().bold(),
            stats.removed.len()
        );
        for f in &stats.removed {
            println!("    {}", style(f).red());
        }
    }

    if !stats.skipped_custom.is_empty() {
        println!(
            "\n  {} {} template(s) personnalise(s) preserve(s) :",
            style("⚠").yellow().bold(),
            stats.skipped_custom.len()
        );
        for f in &stats.skipped_custom {
            println!("    {}", style(f).yellow());
        }
    }

    if stats.unchanged > 0 {
        println!(
            "\n  {} {} fichier(s) identiques (non modifies)",
            style("=").dim(),
            stats.unchanged
        );
    }

    let _ = (project_dir, custom_templates); // utilises dans le plan

    println!("\n{divider}");
    println!("  {}", style("Prochaines etapes :").bold());
    println!("{divider}\n");

    println!("  {}  Verifier la compilation :", style("1.").cyan().bold());
    println!(
        "      {}",
        style(format!("cd {} && cargo check", project_dir.display())).green()
    );
    println!(
        "  {}  Corriger les eventuels breaking changes :",
        style("2.").cyan().bold()
    );
    println!("      {}", style("cargo clippy -- -D warnings").green());
    println!("  {}  Verifier les migrations :", style("3.").cyan().bold());
    println!(
        "      {}",
        style("cargo run -p rustwarden-core --bin migrate").green()
    );
    println!("  {}  Lancer les tests :", style("4.").cyan().bold());
    println!("      {}", style("cargo test").green());
    println!();
    println!(
        "  • Nouveaux écrans éventuels : {} (puis {})",
        style("cargo run -p rustwarden-core --bin screen-select").green(),
        style("-- --check").green()
    );
    println!();

    if !newly_added.is_empty() {
        let divider = style("─".repeat(55)).dim();
        println!("{divider}");
        println!(
            "  {}",
            style("Etapes supplementaires pour la federation :")
                .yellow()
                .bold()
        );
        println!("{divider}\n");

        println!("  {} crate(s) ajoutee(s) :", style("+").green().bold());
        for c in newly_added {
            println!("    - crates/{c}/");
        }

        println!(
            "\n  {}  Ces crates ont ete copiees mais ne sont pas encore",
            style("!").yellow().bold()
        );
        println!("      cablees. Pour les activer dans votre app :");
        println!();
        println!(
            "      {}  Ajouter dans {} :",
            style("a)").cyan(),
            style("Cargo.toml").white()
        );
        if newly_added.contains(&"rustwarden-fediverse") {
            println!(
                "         {}",
                style(
                    "[features]\n         \
                    fediverse = [\"dep:rustwarden-fediverse\"]"
                )
                .green()
            );
        }
        if newly_added.contains(&"rustwarden-fediverse-ui") {
            println!(
                "         {}",
                style("fediverse-ui = [\"fediverse\", \"dep:rustwarden-fediverse-ui\"]").green()
            );
        }
        if newly_added.contains(&"rustwarden-fediverse-guard") {
            println!(
                "         {}",
                style("fediverse-guard = [\"fediverse\", \"dep:rustwarden-fediverse-guard\"]")
                    .green()
            );
        }
        println!();
        println!(
            "      {}  Reproduire le cablage de reference dans src/main.rs",
            style("b)").cyan()
        );
        println!("         (FediverseConfig::from_env, LocalUserResolver,");
        println!("          SignatureVerifier, register_revocation_checker,");
        println!("          merge_routes, OutboundWorker dans tokio::spawn).");
        println!();
        println!(
            "      {}  Configurer les variables d'environnement requises :",
            style("c)").cyan()
        );
        println!(
            "         {}",
            style("FEDIVERSE_DOMAIN, ENCRYPTION_KEY, JWT_SECRET").green()
        );
        println!();
        println!(
            "      {}  Appliquer les migrations fediverse :",
            style("d)").cyan()
        );
        println!(
            "         {}",
            style("cargo run -p rustwarden-core --bin migrate").green()
        );
        println!();
        println!(
            "  {}  Guide complet : {}",
            style("→").dim(),
            style("docs/fediverse-consumer-guide.md")
                .cyan()
                .underlined()
        );
        println!(
            "  {}  Reference API : {}",
            style("→").dim(),
            style("docs/SERVICES_REFERENCE.md §21").cyan().underlined()
        );
        println!();
    }
}

// ---------------------------------------------------------------------------
// Backup
// ---------------------------------------------------------------------------

/// Cree un backup des crates du framework dans le projet.
fn create_backup(project_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
    let backup_dir = project_dir.join(format!(".rustwarden-backup-{timestamp}"));
    fs::create_dir_all(&backup_dir)?;

    println!(
        "\n  {} {}",
        style("Backup").dim(),
        style(backup_dir.display().to_string()).white()
    );

    for crate_name in FRAMEWORK_CRATES {
        let src = project_dir.join("crates").join(crate_name);
        if src.exists() {
            let dst = backup_dir.join("crates").join(crate_name);
            copy_dir_recursive(&src, &dst)?;
            println!("    {} crates/{crate_name}/", style("→").dim(),);
        }
    }

    Ok(backup_dir)
}

// ---------------------------------------------------------------------------
// Mise a jour
// ---------------------------------------------------------------------------

/// Met a jour les crates du framework via sync fichier par fichier.
fn update_crates(
    rustwarden_root: &Path,
    project_dir: &Path,
    has_ui: bool,
    has_fediverse: bool,
    has_fediverse_ui: bool,
    has_fediverse_guard: bool,
    custom_templates: &[String],
) -> Result<SyncStats, Box<dyn std::error::Error>> {
    println!("\n{}", style("── Mise a jour des crates ──").cyan().bold());

    let mut stats = SyncStats::default();

    // rustwarden-core : toujours mis a jour
    sync_crate(
        rustwarden_root,
        project_dir,
        "rustwarden-core",
        &[],
        &mut stats,
    )?;

    // rustwarden-ui : uniquement si le projet l'utilise
    if has_ui {
        sync_crate(
            rustwarden_root,
            project_dir,
            "rustwarden-ui",
            custom_templates,
            &mut stats,
        )?;
    }

    // rustwarden-fediverse : uniquement si le projet l'utilise
    if has_fediverse {
        sync_crate(
            rustwarden_root,
            project_dir,
            "rustwarden-fediverse",
            &[],
            &mut stats,
        )?;
    }

    // rustwarden-fediverse-ui : uniquement si le projet l'utilise
    if has_fediverse_ui {
        sync_crate(
            rustwarden_root,
            project_dir,
            "rustwarden-fediverse-ui",
            &[],
            &mut stats,
        )?;
    }

    // rustwarden-fediverse-guard : uniquement si le projet l'utilise
    if has_fediverse_guard {
        sync_crate(
            rustwarden_root,
            project_dir,
            "rustwarden-fediverse-guard",
            &[],
            &mut stats,
        )?;
    }

    Ok(stats)
}

/// Resynchronise les documents framework-owned dans `<project>/docs/`.
///
/// Ces fichiers ne sont jamais customisés par le consommateur ; ils sont
/// systématiquement réécrits à l'identique de l'upstream à chaque update,
/// afin que la documentation de référence reste alignée avec la version
/// installée des crates. Le contenu fediverse n'est copié que si le projet
/// utilise effectivement la fédération.
fn refresh_docs(
    rustwarden_root: &Path,
    project_dir: &Path,
    has_fediverse: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut docs_to_copy: Vec<&str> = vec!["docs/SERVICES_REFERENCE.md", "docs/MIGRATION_GUIDE.md"];
    if has_fediverse {
        docs_to_copy.push("docs/fediverse-consumer-guide.md");
    }

    let dst_dir = project_dir.join("docs");
    fs::create_dir_all(&dst_dir)?;

    println!(
        "\n{}",
        style("── Resync des docs framework ──").cyan().bold()
    );

    for doc in &docs_to_copy {
        let src = rustwarden_root.join(doc);
        let Some(filename) = Path::new(doc).file_name() else {
            continue;
        };
        let dst = dst_dir.join(filename);

        if !src.exists() {
            println!(
                "  {} {} (source manquante, ignoré)",
                style("!").yellow(),
                style(format!("docs/{}", filename.to_string_lossy())).dim()
            );
            continue;
        }

        // Si le fichier existe et a le même contenu, ne rien faire ;
        // sinon overwrite. Pas de notion de "custom" : ces docs sont
        // framework-owned, jamais éditées par le consommateur.
        let need_write = match (fs::read(&src), fs::read(&dst)) {
            (Ok(s), Ok(d)) => s != d,
            (Ok(_), Err(_)) => true, // dst missing
            _ => false,
        };

        if need_write {
            fs::copy(&src, &dst)?;
            println!(
                "  {} docs/{}",
                style("✓").green().bold(),
                style(filename.to_string_lossy()).cyan()
            );
        } else {
            println!(
                "  {} docs/{} (déjà à jour)",
                style("=").dim(),
                style(filename.to_string_lossy()).dim()
            );
        }
    }

    Ok(())
}

/// Synchronise un crate du framework fichier par fichier.
/// - Fichiers identiques : ignores
/// - Fichiers modifies dans le framework : mis a jour
/// - Fichiers nouveaux dans le framework : ajoutes
/// - Fichiers supprimes du framework : supprimes (sauf custom)
/// - Templates personnalises : preserves
fn sync_crate(
    rustwarden_root: &Path,
    project_dir: &Path,
    crate_name: &str,
    custom_templates: &[String],
    stats: &mut SyncStats,
) -> Result<(), Box<dyn std::error::Error>> {
    let src = rustwarden_root.join("crates").join(crate_name);
    let dst = project_dir.join("crates").join(crate_name);

    if !src.exists() {
        return Err(format!("Crate source introuvable : {}", src.display()).into());
    }

    let prefix = format!("crates/{crate_name}");

    // 1. Copier/mettre a jour depuis la source
    sync_dir_recursive(&src, &dst, &prefix, crate_name, custom_templates, stats)?;

    // 2. Supprimer les fichiers du projet qui n'existent plus dans le framework
    cleanup_removed_files(&src, &dst, &prefix, crate_name, custom_templates, stats)?;

    println!("  {} crates/{crate_name}/", style("✓").green().bold());

    Ok(())
}

/// Synchronise recursivement un dossier source vers un dossier destination.
fn sync_dir_recursive(
    src: &Path,
    dst: &Path,
    prefix: &str,
    crate_name: &str,
    custom_templates: &[String],
    stats: &mut SyncStats,
) -> Result<(), Box<dyn std::error::Error>> {
    fs::create_dir_all(dst)?;

    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let file_type = entry.file_type()?;
        let src_path = entry.path();
        let dst_path = dst.join(entry.file_name());
        let name = entry.file_name().to_string_lossy().to_string();

        // Exclure target/, .git/, data/
        if name == "target" || name == ".git" || name == "data" {
            continue;
        }

        let relative = format!("{prefix}/{name}");

        if file_type.is_dir() {
            sync_dir_recursive(
                &src_path,
                &dst_path,
                &relative,
                crate_name,
                custom_templates,
                stats,
            )?;
        } else {
            // Verifier si c'est un template personnalise a preserver
            if is_custom_template(&dst_path, crate_name, custom_templates) {
                stats.skipped_custom.push(relative.clone());
                continue;
            }

            if dst_path.exists() {
                // Comparer le contenu
                let src_content = fs::read(&src_path)?;
                let dst_content = fs::read(&dst_path)?;

                if src_content == dst_content {
                    stats.unchanged += 1;
                } else {
                    fs::copy(&src_path, &dst_path)?;
                    stats.updated.push(relative);
                }
            } else {
                // Nouveau fichier
                if let Some(parent) = dst_path.parent() {
                    fs::create_dir_all(parent)?;
                }
                fs::copy(&src_path, &dst_path)?;
                stats.added.push(relative);
            }
        }
    }

    Ok(())
}

/// Supprime les fichiers du projet qui n'existent plus dans le framework.
fn cleanup_removed_files(
    src: &Path,
    dst: &Path,
    prefix: &str,
    crate_name: &str,
    custom_templates: &[String],
    stats: &mut SyncStats,
) -> Result<(), Box<dyn std::error::Error>> {
    if !dst.exists() {
        return Ok(());
    }

    for entry in fs::read_dir(dst)? {
        let entry = entry?;
        let file_type = entry.file_type()?;
        let src_path = src.join(entry.file_name());
        let dst_path = entry.path();
        let name = entry.file_name().to_string_lossy().to_string();

        if name == "target" || name == ".git" || name == "data" {
            continue;
        }

        let relative = format!("{prefix}/{name}");

        if file_type.is_dir() {
            if src_path.exists() {
                // Le dossier existe dans le framework, verifier son contenu
                cleanup_removed_files(
                    &src_path,
                    &dst_path,
                    &relative,
                    crate_name,
                    custom_templates,
                    stats,
                )?;
            }
            // On ne supprime pas les dossiers entiers qui n'existent pas
            // dans le framework — ils peuvent contenir des fichiers custom
        } else if !src_path.exists() {
            // Le fichier n'existe plus dans le framework
            if is_custom_template(&dst_path, crate_name, custom_templates) {
                // Template custom, on le garde
                continue;
            }
            fs::remove_file(&dst_path)?;
            stats.removed.push(relative);
        }
    }

    Ok(())
}

/// Verifie si un fichier du projet est un template personnalise.
fn is_custom_template(file_path: &Path, crate_name: &str, custom_templates: &[String]) -> bool {
    if crate_name != "rustwarden-ui" || custom_templates.is_empty() {
        return false;
    }

    // Extraire le chemin relatif depuis templates/
    let path_str = file_path.to_string_lossy();
    let Some(idx) = path_str.find("templates/") else {
        return false;
    };
    let relative = &path_str[idx + "templates/".len()..];

    custom_templates.iter().any(|t| t == relative)
}

// ---------------------------------------------------------------------------
// Decouverte du workspace Rustwarden
// ---------------------------------------------------------------------------

/// Cherche la racine du workspace Rustwarden (le framework source).
fn find_rustwarden_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let start_dirs = [
        std::env::current_exe()
            .ok()
            .and_then(|p| p.parent().map(|p| p.to_path_buf())),
        std::env::current_dir().ok(),
    ];

    for start in start_dirs.into_iter().flatten() {
        let mut dir = start;
        loop {
            let cargo_toml = dir.join("Cargo.toml");
            if cargo_toml.exists() {
                let content = fs::read_to_string(&cargo_toml)?;
                if content.contains("[workspace]")
                    && dir.join("crates/rustwarden-core").exists()
                    && dir.join("tools/rustwarden-cli").exists()
                {
                    return Ok(dir);
                }
            }
            if !dir.pop() {
                break;
            }
        }
    }

    Err("Impossible de trouver la racine du framework Rustwarden.\n\
         Lancez cette commande depuis le repertoire Rustwarden \
         ou son sous-dossier."
        .into())
}

// ---------------------------------------------------------------------------
// Copie recursive
// ---------------------------------------------------------------------------

/// Copie un dossier recursivement en excluant target/, .git/, data/.
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), Box<dyn std::error::Error>> {
    fs::create_dir_all(dst)?;

    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let file_type = entry.file_type()?;
        let src_path = entry.path();
        let dst_path = dst.join(entry.file_name());

        let name = entry.file_name().to_string_lossy().to_string();
        if name == "target" || name == ".git" || name == "data" {
            continue;
        }

        if file_type.is_dir() {
            copy_dir_recursive(&src_path, &dst_path)?;
        } else {
            fs::copy(&src_path, &dst_path)?;
        }
    }

    Ok(())
}